C++学习笔记(8)
学习是一件任重而道远的事情,与其焦虑不如动手起来,借助平台记录自己学习笔记,希望和大家多多交流,今天又是努力成为程序媛的一天!
14.程序的内存模型
本阶段主要针对C++面向对象编程技术做详细讲解
- 内存分区模型(C++程序执行时,将内存大方向划分为4个区域)
- 代码区:存放函数的二进制代码,由操作系统进行管理
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
意义:不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程
程序编译后,生成exe可执行程序,未执行程序前分为两个区域:代码区和全局区 -
14.1代码区
存放CPU执行的机器指令
代码是共享的:目的是对于频繁执行的程序,只需要在内存中有一份代码即可
代码是只读的:防止程序意外修改它的指令
14.2全局区
全局变量和静态变量存放在此
还包含常量区,字符常量和其他常量也在
该区域的数据在程序结束后由操作系统释放
#include<iostream>
using namespace std;
#include<string>
//创建一个全局变量//只要不在函数体里的变量就是全局变量
int g_a = 20;
int g_b = 20;
//const修饰的全局常量
const int c_g_a = 10;
const int c_g_b = 10;
int main() {
//全局区
//全局变量 静态变量 常量
//创建一个非全局区的普通局部变量
int a = 10;
int b = 10;
cout << "非全局区的局部变量a的地址为:" << (long long)&a << endl;
cout << "非全局区的局部变量b的地址为:" << (long long)&b << endl;
cout << "全局变量g_a的地址为:" << (long long)&g_a << endl;
cout << "全局变量g_b的地址为:" << (long long)&g_b << endl;
//静态变量 在普通变量前加static,属于静态变量
static int s_a = 10;
static int s_b = 10;
cout << "静态变量s_a的地址为" << (long long)&s_a << endl;
cout << "静态变量s_b的地址为" << (long long)&s_b << endl;
//常量
//字符串常量
cout << "字符串常量的地址为:" << (long long)&"hello world" << endl;
//const修饰的变量
//const修改的全局变量
cout << "全局常量 c_g_a的地址为:" << (long long)&c_g_a << endl;
cout << "全局常量 c_g_b的地址为:" << (long long)&c_g_b << endl;
//const修饰的局部变量
const int c_l_a = 10;
const int c_l_b = 10;
cout << "const修饰的局部变量 c_l_a的地址为:" << (long long)&c_l_a << endl;
cout << "const修饰的局部变量 c_l_b的地址为:" << (long long)&c_l_b << endl;
system("pause");
return 0;
}
运行结果:
注意:
- 只要不在函数体里定义的变量都是全局变量
- 变量名,c-const;g-global;l-local;const修饰的变量,全局变量,局部变量
- 静态变量即在普通变量前面加static,属于静态变量
- 常量分为以下几种:
- 1.字符串常量 2.const修饰的全局变量 3.const修饰的局部变量
- 根据地址可见,局部变量和全局区地址很远,不在一个区
- 全局区内有:全局变量;静态变量(static);字符串变量。const修饰的全局变量
- 局部变量和const修饰的局部变量不在全局区
14.3栈区
由编译器自动分配释放,存放函数的参数值,局部变量等
注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
#include<iostream>
using namespace std;
//栈区注意事项 -- 不要返回局部变量地址
//栈区的数据由编译器管理开辟和释放
int * func() {//用int指针数据类型来接收地址
int a = 10;//局部变量,存放在栈区,栈区的数据在函数执行完后自动释放
int b = 20;
string name = "abc";//就定一个变量时,地址怎么返回都不变,但是函数稍微复杂点,变量多时就不是正确数字了
char s = 'q';
return &a;//返回局部变量的地址
}
int main() {
int * p = func();//接收函数的返回值//运行完这个函数后内存就被释放了
cout << *p << endl;//第一次可以打印正确数字,因为编译器做了保留
cout << *p << endl;//第二次数据就不再保留了
cout << *p << endl;
cout << *p << endl;
system("pause");
return 0;
}
运行结果:
栈区存放形参(形参开辟的数据)和局部变量,不要返回局部变量的地址,因为局部变量在函数执行完之后就自动释放了,再用指针去操作那个地址时,就是非法操作了。
理解:
几点注意:
- 关于栈区:当一个函数运行结束后,这个函数在栈区所有变量都将被删除,这些变量占用的内存空间也都将被释放,相当于清空和销毁变量
- 在这里,栈区的变量是如何变化的:
首先在栈区创建了一个局部变量,给a一个内存地址存放的值为10,用指针接收返回a的内存地址,然而当主程序运行func()函数时,函数一执行完,那些变量和地址空间都被销毁了,不过当时a的地址返回给了主程序的指针p,但此时p指向的原地址早就没有没有a的值了,所以出来的乱码值,每次调用该函数如此,第一次之所以可以打印正确数字因为编译器做了保留。
14.4堆区
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
在C++中主要利用new在堆区开辟内存
#include<iostream>
using namespace std;
int* func() {
//指针本质是局部变量 放在栈上 指针保存的数据是放在堆区,即堆区创建的变量地址,它是不变的
int* p = new int(10);//利用new关键字 将数据开辟到堆区
int b = 20;
string name = "abs";
return p;
}
int main() {
//在堆区开辟数据
int* p1 = func();
cout << (long long)p1 << endl;
cout << (long long)p1 << endl;
cout << (long long)p1 << endl;
cout << *p1 << endl;
system("pause");
return 0;
}
运行结果:
理解:
几点注意:
- 堆区创建的变量由程序员管理释放的,这其中的变量可以在程序任何地方访问
- 堆区内存访问需要用到指针
- 通常用new()或malloc()在堆区开辟数据,创建之后是返回的地址,所以需要指针来接收它
- 堆区的变量占用的内存地址和值除程序员不会自动改动,即一般是固定的地址
- 在这里,堆区变量是这样变化的:
首先在堆区开辟了一个int的地址空间,存放数值为10,这是不随程序运行改变的,是固定的,然后在栈区创建了一个指针变量的地址,指针变量接收的值就是10对应的那个int地址号,函数最后返回int地址。到主程序时,先是运行函数,完成上述过程,然后返回int地址给指针p1,随着函数运行结束,栈区的变量全部销毁,而p1依旧代表int那个地址号,而用指针去访问那个地址时,就是int地址存放的数据10;而p1始终指向的都是int 那个地址。无论运行多次,结果都是10以及其存放的地址。即栈区变量虽然没了,但是堆区地址和值不变。
14.5new操作符
C++中利用new操作符在堆区开辟数据
堆区开辟的数据,由程序员手动开辟,利用操作符delete释放
创建语法:new 数据类型
利用new创建的数据,会返回该数据对应的类型的指针
#include<iostream>
using namespace std;
#include<string>
//1.new的基本用法
int* func() {
int* p = new int(10);//利用new关键字 将数据开辟到堆区
return p;//new返回的是 该数据类型的指针
}
void test01() {
int* p = func();
cout << *p << endl;
cout << *p << endl;
//如果想释放堆区的数据,利用关键字delete
delete p;
cout << *p << endl;//内存已经被释放,再次访问就是非法操作,会报错
}
void test02() {
//2.在堆区利用new创建数组
int* arr = new int[10];//代表创建10个元素的数组
for (int i = 0; i < 10; i++) {
arr[i] = i;//创建数组值0-9
cout << arr[i] << endl;
}
//如果要删除堆区创建的数组 要加[]才可!!!
delete[] arr;
}
int main() {
//test01();
test02();
system("pause");
return 0;
}
test01运行结果:
test02:
- 如果要删除堆区创建的数组 要加[]才可!!!
15.C++中的引用
15.1 引用的基本使用
作用:给变量起别名
语法:数据类型 &别名 = 原名
#include<iostream>
using namespace std;
#include<string>
int main() {
//引用基本语法
//数据类型 &别名 = 原名
int a = 10;
//创建引用
//就是给变量起别名
//操作原名和别名就是操作同一个变量
int& b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
b = 100;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
system("pause");
return 0;
}
运行结果:
15.2 引用的注意事项
- 引用必须初始化
- 引用初始化后,不可以改变
#include<iostream>
using namespace std;
#include<string>
int main() {
int a = 10;
int& b = a;//初始化
//1.引用必须初始化
//int& b;//错误,必须要初始化
//2.引用在初始化后,不可以改变
int c = 20;
b = c;//这不是在更改引用,而是赋值操作
cout << "a = " << a << endl;//20
cout << "b = " << b << endl;//20
cout << "c = " << c << endl;//20
system("pause");
return 0;
}
运行结果:
- 这里b已经初始化了作为a的别名,后续c相当于赋值给b,而不是更改引用
- 所以c的值赋给了b为20,而b引用a,a也为20
15.3 引用做函数参数
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
#include<iostream>
using namespace std;
#include<string>
//交换函数
//1.值传递
void myswap01(int a,int b) {
int temp = a;
a = b;
b = temp;
cout << "myswap01()中a = " << a << endl;
cout << "myswap01()中b = " << b << endl;
}
//2.地址传递
void myswap02(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
cout << "myswap02()中a = " << *a << endl;
cout << "myswap02()中b = " << *b << endl;
}
//3.引用传递
void myswap03(int& a, int& b) {
int temp = a;
a = b;
b = temp;
cout << "myswap03()中a = " << a << endl;
cout << "myswap03()中b = " << b << endl;
}
int main() {
int a = 10;
int b = 20;
myswap01(a, b);//值传递,形参不会修饰实参
//myswap02(&a, &b);//地址传递,形参会修饰实参
//myswap03(a, b);//引用传递,形参会修饰实参
cout << "a = " << a << endl;
cout << "b = " << b << endl;
system("pause");
return 0;
}
myswap01(a, b)函数交换结果:
myswap02(&a, &b)函数交换结果:
myswap03(a, b)函数交换结果:
引用传递里的形参相当于引用实参a,b修改形参就相当于改变了实参a,b的值
15.4 引用做函数返回值
作用:引用是可以作为函数的返回值存在的
注意:不要返回局部变量引用
因为局部变量在栈区,一旦调用函数结束后,栈区定义的变量就销毁了,主程序引用的即其变量的别名也不在了,即非法访问,没有意义
用法:函数调用作为左值
函数调用返回的变量值作为左值,可以给其赋值,不过记住栈区里变量是无法访问的,销毁后无法访问,非法操作,赋值给函数调用后,相当于改了变量值和别名
#include<iostream>
using namespace std;
#include<string>
//引用做函数的返回值
//1.不要返回局部变量的引用
int &test01() {//用引用方式做一个返回
int a = 10;//局部变量存放在栈区
return a;
}
//2.函数的调用可以作为左值
int& test02() {
static int a = 10;//静态变量在全局区,在程序结束后系统释放
return a;
}
int main() {
int &ref = test01();
cout << "ref = " << ref << endl;//收到的返回值a引用给到ref,而此时a已经销毁了,所以非法操作,乱码值
int& ref2 = test02();
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;//静态变量不会被释放
test02() = 1000;
cout << "赋值操作1000给函数调用返回值a" << endl;
cout << "ref2 = " << ref2 << endl;//函数调用做左值,并赋值操作,别名访问也是1000了
cout << "ref2 = " << ref2 << endl;
system("pause");
return 0;
}
15.5 引用本质
本质在C++内部实现是一个指针常量
指针常量具体可见C++笔记指针
15.6 常量引用
作用:常量引用主要用来修饰形参,防止误操作
在函数形参列表中,可以加const修饰形参,防止形参改变实参
这里相当于指针常量第二种情况
16.函数提高
16.1 函数默认参数
在C++中,函数的形参列表中的形参是可以有默认值的
语法:返回值类型 函数名 (参数 = 默认值){}
#include<iostream>
using namespace std;
#include<string>
int function(int a,int b = 10, int c = 30) {//在函数定义时就给形参赋值,即默认参数
return a + b + c;
}
//几点注意:
//1.如果某个位置参数有默认值,那么从这个位置往后都必须要有默认值 下面这样是错的
//int function(int a= 10, int b , int c = 30) {
//
// return a + b + c;
//}
//2.如果函数声明有默认值,那么函数定义时就不能有默认参数
//即使暂时不报错,但是调用函数时就有问题了 声明和定义只能有一个是有默认参数的 否则编译器不知道听谁的 二义性
//int function(int a, int b = 10, int c = 30);
int main() {
//函数的默认参数
int res = function(10, 20);//如果实参有值,就用实参值,如果没有就用形参默认参数值,可以全部默认参数,这样实参可以不加,返回默认值
//如果实参有定义值,形参也有定义,以实参的计算,没有的按形参默认参数值算
//这里相当于a=10,b=20,c=30
cout << res << endl;//60
system("pause");
return 0;
}
- 如果某个位置参数有默认值,那么从这个位置往后都必须要有默认值
- 如果函数声明有默认值,那么函数定义时就不能有默认参数
- 如果实参有定义值,形参也有定义,以实参的计算,没有的按形参默认参数值算
16.2 函数的占位参数
C++中函数的形参列表可以有占位参数,用来做占位,调用函数时必须填补该位置
语法:返回值类型 函数名(数据类型){}
#include<iostream>
using namespace std;
#include<string>
//占位参数
//返回值类型 函数名(数据类型)
//占位参数也可以有默认参数
//void func(int a,int ) {
void func(int a, int =10) {
cout << "this is a test" << endl;
}
int main() {
//func(10, 10);//占位参数调用时也要填补实参
func(10);//占位参数已经有了默认参数,这里可以省略
system("pause");
return 0;
}
16.3 函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数类型不同 或者个数不同 或者顺序不同
注意:函数的返回值不可以作为函数重载的条件
16.3.1 基本语法
#include<iostream>
using namespace std;
#include<string>
//函数重载
//满足条件:
//1.同一个作用域下 //这里都在全局区
//2.函数名称相同
//3.函数参数类型不同 /个数不同 / 顺序不同
void func() {
cout << "func的调用" << endl;//第一个
}
void func(int a) {
cout << "func(int a)的调用" << endl;//第二个
}
//第一个和第二个参数个数不同,函数名相同,成为函数重载,可以
void func(double a) {
cout << "func(double a)的调用" << endl;//第三个
}
//第二个和第三个参数类型不同,函数重载
void func(double a,int b) {
cout << "func(double a,int b)的调用" << endl;//第四个
}
void func(int a, double b) {
cout << "func(int a,double b)的调用" << endl;//第五个
}
//第四个和第五个参数顺序不同,也可
//注意:函数返回值不可以作为函数重载的条件
//int func(int a) {
// cout << "func(int a)的调用" << endl;//第六个
// return -1;
//}
//
//
//string func(int a) {
// cout << "func(int a)的调用" << endl;//第七个
// return "abs";
//}
//第六个和第七个都是不行的
//函数有返回值和无返回值不可构成函数重载,即函数返回值不可作为函数重载条件
//函数都有返回值,返回值类型不同也不可以作为函数重载条件
int main() {
func();
func(10);
func(3.15);
func(3.15, 10);
func(10, 3.45);
system("pause");
return 0;
}
16.3.2 注意事项
- 引用作为重载条件
- 函数重载碰到函数默认参数
#include<iostream>
using namespace std;
//函数重载注意事项
//1.引用作为重载的条件
//这里二者参数类型不同 加/不加const可以作为重载条件
void func(int& a) {//不能int &a= 10;非法访问,必须引用的是一个合理的空间
cout << "func(int& a)的调用" << endl;
}
void func(const int& a) {//可以const int &a = 10,常量,编译器优化,int temp = 10,const int &a = temp;
cout << "func(const int& a)的调用" << endl;
}
//2.函数重载碰到默认参数
void func2(int a, int b = 10) {
cout << "func2(int a, int b = 10)的调用" << endl;
}
void func2(int a) {
cout << "func2(int a)的调用" << endl;
}
//以上func2()本身重载合理,但是调用时如果只给一个值,上面两个都可,出现二义性
int main() {
int a = 10;
func(a);
cout << "func(a)是调用的func(int& a)函数" << endl;
func(20);
cout << "func(20)是调用的func(const int& a)函数" << endl;
//func2(12);//函数重载又有默认参数,出现二义性,报错,尽量避免,可以传上面也可以传下面func2()函数,所以二义性报错
system("pause");
return 0;
}
第八篇笔记到此结束,C++基础学习会持续更新在C++学习笔记合集中,当作学习笔记复习,如果能帮助其他小伙伴就更好了。
笔记是看黑马程序C++时做的记录,笔记中如果有错误和改进的地方,欢迎大家评论交流,up up up!!!
学习原视频来自:黑马程序员C++从0到1