【C++学习】核心编程之内存分区模型、引用和函数提高(黑马学习笔记)
目录
一、内存分区模型
1.1程序运行前
1.2程序运行后
1.3new操作符
二、引用
2.1基本使用
2.2引用做函数参数
2.3引用做函数返回值
2.4引用的本质
2.5常量引用
三、函数提高
3.1函数默认参数
3.2函数占位参数
3.3函数重载
系列文章:
一、内存分区模型
C++程序在执行时,将内存大方向划分为4个区域
-
代码区:存放函数体的二进制代码,由操作系统进行管理的
-
全局区:存放全局变量和静态变量以及常量
-
栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
-
堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
内存四区意义:
不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程
1.1程序运行前
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
代码区:
存放 CPU 执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可【共享特性】
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令【只读特性】
全局区:
全局变量和静态变量存放在此.
全局区还包含了常量区, 字符串常量和其他常量也存放在此.
该区域的数据在程序结束后由操作系统释放【内存管理】
1. 全局变量:在所有函数外部定义的变量。
可以在程序的任何地方访问,具有全局作用域;程序开始时分配内存,程序结束时释放。
全局变量在整个程序的执行过程中保持其值,可以在多个地方被修改。
2. 静态变量:使用
static
关键字定义的变量,可以是局部的也可以是全局的。局部静态变量:在函数内部定义,生命周期与全局变量相同,但作用域限于定义它的函数。
全局静态变量:在所有函数外部定义,并且只能在定义它的文件中访问。
程序开始时分配内存,程序结束时释放,保持其值直到下次调用。
3. 常量:使用
const
关键字定义的不可修改的变量。常量可以是全局的、局部的,也可以是静态的。
#include <iostream>
using namespace std;// 全局变量
int g_a = 10; // 全局变量在整个程序中都可以访问
int g_b = 10; const int c_g_a = 10; // 全局常量值不可更改
const int c_g_b = 10;int main()
{// 局部变量int a = 10; // 局部变量 a,仅在 main 函数内有效int b = 10; // 打印局部变量的地址cout << "局部变量a地址为: " << (int)&a << endl; cout << "局部变量b地址为: " << (int)&b << endl; // 打印全局变量的地址cout << "全局变量g_a地址为: " << (int)&g_a << endl; cout << "全局变量g_b地址为: " << (int)&g_b << endl; // 静态变量// 静态变量值在函数调用之间保持不变static int s_a = 10; static int s_b = 10; // 打印静态变量的地址cout << "静态变量s_a地址为: " << (int)&s_a << endl; cout << "静态变量s_b地址为: " << (int)&s_b << endl; // 打印字符串常量的地址cout << "字符串常量地址为: " << (int)&"hello world" << endl; cout << "字符串常量地址为: " << (int)&"hello world1" << endl; // 打印全局常量的地址cout << "全局常量c_g_a地址为: " << (int)&c_g_a << endl; cout << "全局常量c_g_b地址为: " << (int)&c_g_b << endl;// 局部常量const int c_l_a = 10; // 局部常量 c_l_a,仅在 main 函数内有效,值不可更改const int c_l_b = 10; // 打印局部常量的地址cout << "局部常量c_l_a地址为: " << (int)&c_l_a << endl; cout << "局部常量c_l_b地址为: " << (int)&c_l_b << endl; system("pause"); return 0;
}
运行结果:
全局变量存放在全局区(数据区),地址相邻(g_a 和 g_b 地址差为 4,因它们都是 int
类型)。
局部变量和局部常量在栈区。
全局变量、静态变量和全局常量在全局区
1.2程序运行后
栈区:
由编译器自动分配释放, 存放函数的参数值,局部变量等
注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
#include <iostream>
using namespace std;// 函数返回局部变量的地址
int* func() {int a = 10; // 局部变量 a,存储在栈区return &a; // 返回局部变量 a 的地址
}int main() {int* p = func(); // p 接收 func() 的返回值(局部变量 a 的地址)// 这里尝试解引用 p,访问局部变量 a 的值cout << *p << endl; cout << *p << endl; system("pause"); return 0;
}
在 C++ 中,返回局部变量的地址是非常危险的做法,会导致程序的稳定性和安全性问题。
return &a;
返回局部变量的地址。这是一个常见的错误,因为当func
返回时,a
的内存会被释放,指针p
将指向一个无效的地址
vs2022运行出来两次都是10是vs2022编译器做了优化,故和老师的视频运行结果不一样
堆区:
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
在C++中主要利用new在堆区开辟内存
#include <iostream>
using namespace std;// 函数返回一个动态分配的整数指针
int* func()
{//利用new关键字,将数据开辟到堆区//指针本质是局部变量,放在栈上,指针保存的数据放在堆区int* a = new int(10); // 动态分配内存,初始化为 10return a; // 返回指向动态分配内存的指针,存放地址
}int main()
{int* p = func(); // p 接收 func() 的返回值,即动态分配的整数指针// 输出指针 p 指向的值cout << *p << endl; // 输出 10cout << *p << endl; // 再次输出 10delete p; // 释放动态分配的内存,以避免内存泄漏system("pause");return 0;
}
指针本质是局部变量,放在栈上,指针保存的数据放在堆区
指针的性质:
int* a;
是一个指针变量,它本身是在栈区分配的。它在func
函数调用期间存在,函数返回后,指针本身的内存会被释放。指针指向的内存:
指针
a
保存的是指向堆区动态分配内存的地址。int* a = new int(10);
创建了一个存储10
的整型变量,这个整型变量位于堆区。尽管指针变量a
存储在栈区,但它指向的数据(10
)在堆区,直到显式释放(使用delete
)为止
new关键字可以创建一个 堆区数据,返回创建的数据的地址
1.3new操作符
C++中利用new操作符在堆区开辟数据
堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete
语法: new 数据类型
#include <iostream>
using namespace std;int main() {// 在堆区动态分配一个整型变量int* p = new int(10); // 使用 new 分配内存并初始化为 10// 输出指针 p 指向的值cout << *p << endl; // 输出 10// 释放动态分配的内存delete p; // 在堆区动态分配一个整型数组int* arr = new int[5]; // 使用 new[] 开辟一个包含 5 个整数的数组// 初始化数组for (int i = 0; i < 5; i++) {arr[i] = i * 2; // 将数组初始化为 0, 2, 4, 6, 8}// 输出数组中的值for (int i = 0; i < 5; i++) {cout << arr[i] << " "; // 输出 0 2 4 6 8}cout << endl;delete[] arr; // 使用 delete[] 释放数组的内存return 0;
}
利用new创建的数据,会返回该数据对应的类型的指针
动态分配单个变量:
int* p = new int(10);
在堆上分配一个整型变量并初始化为 10
,p
存储了这个变量的地址。
动态分配数组:
int* arr = new int[5];
在堆上分配一个整型数组,数组包含 5 个整数。
释放内存:
使用 delete
释放单个变量的内存,使用 delete[]
释放数组的内存。
二、引用
2.1基本使用
作用: 给变量起别名
语法: 数据类型 &别名 = 原名
#include <iostream>
using namespace std;int main() {int a = 10; // 定义整型变量 a,并初始化为 10int& b = a; // 定义引用 b,引用变量 acout << "a = " << a << endl; cout << "b = " << b << endl; b = 100; // 修改引用 b 的值cout << "a = " << a << endl; cout << "b = " << b << endl; system("pause"); return 0;
}
引用的作用:b
和 a
实际上指向同一内存地址,对 b
的修改会影响到 a
。因此,当你通过 b
修改值时,a
的值也会改变。
运行结果:修改 b
为 100
后,a
也变为 100
,因为 b
是对 a
的引用
int a = 10;int b = 20;//int &c; //错误,引用必须初始化int &c = a; //一旦初始化后,就不可以更改c = b; //这是赋值操作,不是更改引用
引用注意事项
- 引用必须初始化
- 引用在初始化后,不可以改变
2.2引用做函数参数
函数传参时,可以利用引用的技术让形参修饰实参
#include <iostream>
using namespace std;// 1. 值传递
void mySwap01(int a, int b) {int temp = a; // 将 a 的值保存在 temp 中a = b; // 将 b 的值赋给 ab = temp; // 将 temp 的值赋给 b
}// 2. 地址传递
void mySwap02(int* a, int* b) {int temp = *a; // 通过指针获取 a 指向的值*a = *b; // 将 b 指向的值赋给 a 指向的地址*b = temp; // 将 temp 的值赋给 b 指向的地址
}// 3. 引用传递
void mySwap03(int& a, int& b) {int temp = a; // 将 a 的值保存在 temp 中a = b; // 将 b 的值赋给 ab = temp; // 将 temp 的值赋给 b
}int main() {int a = 10; int b = 20; mySwap01(a, b); // 调用值传递交换函数cout << "a:" << a << " b:" << b << endl; mySwap02(&a, &b); // 调用地址传递交换函数cout << "a:" << a << " b:" << b << endl; mySwap03(a, b); // 调用引用传递交换函数cout << "a:" << a << " b:" << b << endl; system("pause"); return 0;
}
- 值传递适合不需要修改原始数据的情况。
- 地址传递适合需要在函数内部修改数据的情况,但要小心指针的使用。
- 引用传递提供了一种简洁的方法来实现类似于指针的功能,同时提高了代码的可读性
值传递、引用传递、指针传递【函数的参数传递】详解-CSDN博客
2.3引用做函数返回值
注意:不要返回局部变量引用
当一个函数返回一个引用时,它就可以作为左值使用
#include <iostream>
using namespace std;// 返回局部变量引用
int& test01() {int a = 10; // 局部变量,存放在栈区return a; // 返回局部变量的引用(函数返回值类型int&)【错误用法】
}// 返回静态变量引用
int& test02() {static int a = 20; // 静态变量,放在全局区,生命周期贯穿程序运行return a; // 返回静态变量的引用【正确用法】
}int main() {// 不能返回局部变量的引用int& ref = test01(); // 尝试返回局部变量的引用,会导致未定义行为cout << "ref = " << ref << endl; cout << "ref = " << ref << endl; // 如果函数做左值,那么必须返回引用int& ref2 = test02(); // 返回静态变量的引用cout << "ref2 = " << ref2 << endl; // 输出 ref2 的值,结果为 20cout << "ref2 = " << ref2 << endl; //函数调用做左值test02() = 1000; // 通过引用修改静态变量 a 的值cout << "ref2 = " << ref2 << endl; // 输出 ref2 的值,结果为 1000cout << "ref2 = " << ref2 << endl; system("pause"); return 0;
}
2.4引用的本质
引用的本质在c++内部实现是一个指针常量.
#include <iostream>
using namespace std;// 函数接受一个引用参数
void func(int& ref) {ref = 100; // ref 是引用,实际效果是 *ref = 100;
}int main() {int a = 10; // 声明一个引用 ref,指向 a//自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改int& ref = a; ref = 20; // 修改 ref 的值,实际上是修改 a 的值,*ref = 20;cout << "a: " << a << endl; // 输出 a: 20cout << "ref: " << ref << endl; // 输出 ref: 20func(a); // 调用函数,传入 a 的引用cout << "a after func: " << a << endl; // 输出 a : 100cout << "ref after func: " << ref << endl; // 输出 ref : 100return 0;
}
C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了
2.5常量引用
主要用来修饰形参,防止误操作(在函数形参列表中,加const修饰形参,防止形参改变实参)
#include <iostream>
using namespace std;// 常量引用参数
void showValue(const int& v) {// v += 10; // 不能修改常量引用的值cout << v << endl; // 输出引用的值
}int main() {// int& ref = 10; // 错误:引用必须指向一个合法的内存空间// 加入 const 使其合法,编译器优化生成临时变量int temp = 10; const int& ref = temp;const int& ref = 10; // 创建一个常量引用,指向字面量 10// ref = 100; // 错误:常量引用不能被修改cout << ref << endl; // 结果为 10int a = 10;showValue(a); // 调用函数,传入 a 的常量引用system("pause");return 0;
}
C++中的引用——引用详解_c++工程引用-CSDN博客
三、函数提高
3.1函数默认参数
在C++中,函数的形参列表中的形参是可以有默认值的。
语法: 返回值类型 函数名 (参数= 默认值){}
#include <iostream>
using namespace std;// 带有默认参数的函数
int func(int a, int b = 10, int c = 10) {return a + b + c;
}// 函数声明,带有默认参数【 如果函数声明有默认值,函数实现的时候就不能有默认参数】
// 注意:如果某个参数有默认值,那么从这个参数往后必须都要有默认值
int func2(int a = 10, int b = 10); // 函数声明
int func2(int a, int b) { // 函数实现return a + b;
}int main() {cout << "ret = " << func(20, 20) << endl; // 输出:ret = 50cout << "ret = " << func(100) << endl; // 输出:ret = 120system("pause");return 0;
}
如果某个位置的参数有默认值,则从该参数往后,所有参数都必须有默认值
如果我们自己传入参数,就用自己的数据,没有的话就用默认值
在函数的声明和定义中,不能同时包含默认参数。如果函数声明中已有默认参数,则在实现时不能再次声明它们
3.2函数占位参数
C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置
语法: 返回值类型 函数名 (数据类型){}
#include <iostream>
using namespace std;// 定义一个函数,其中有一个占位参数
void func(int a, int) {cout << "this is func" << endl; // 打印信息
}int main() {// 调用函数,传入两个参数func(10, 10); // 占位参数必须填补system("pause");return 0;
}
占位参数定义:
void func(int a, int)
定义了一个函数func
,其中第二个参数是占位参数。这个参数虽然定义了,但在函数体内没有被使用函数调用:
在
main
中调用func(10, 10);
时,两个参数都必须提供,即使第二个参数没有被使用。C++ 的语法要求你必须为每个参数提供一个值
3.3函数重载
函数名可以相同,提高复用性
函数重载满足条件:
-
同一个作用域下
-
函数名称相同
-
函数参数类型不同 或者 个数不同 或者 顺序不同
函数的返回值不可以作为函数重载的条件
注意事项
- 引用作为重载条件
- 函数重载碰到函数默认参数
#include <iostream>
using namespace std;// 函数重载示例:引用作为重载条件
void func(int &a) {cout << "func (int &a) 调用 " << endl; void func(const int &a) {cout << "func (const int &a) 调用 " << endl;
}// 函数重载示例:处理默认参数
void func2(int a, int b = 10) {cout << "func2(int a, int b = 10) 调用" << endl;
}void func2(int a) {cout << "func2(int a) 调用" << endl;
}int main() {int a = 10;// 调用带有引用的函数func(a); // 调用 func(int &a)func(10); // 调用 func(const int &a)// 以下调用会导致歧义,因为存在默认参数// func2(10); // 碰到默认参数产生歧义,需要避免system("pause");return 0;
}
系列文章:
基于基于哔哩哔哩黑马程序员的学习笔记
在本博文前:
C++学习笔记(基于哔哩哔哩黑马程序员)之基础编程(上)
C++学习笔记(基于哔哩哔哩黑马程序员)之基础编程(下)
C++学习(黑马程序员)之通讯录管理系统(附完整代码)【代码实战】
视频链接:黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难_哔哩哔哩_bilibili