C++学习第四篇(内存分区模型、引用、函数高级)

一,C++内存分区模型:

1,代码区,存放函数体的二进制代码,由操作系统管理;
2,全局区,存放全局变量、静态变量和常量;
3,栈区,由编译器自动编译、释放,存放函数的参数、局部变量等;
4,堆区,由程序员分配和释放,若程序员不手动释放,则会在程序结束时由操作系统自动释放。

1,程序运行之前:

在程序运编译成exe可执行文件之后,但还没有双击exe跑起来之前,只分为两个区域:
代码区:
存放cpu执行的指令;
该区域是共享的,只会存储一份,不论将来跑多少遍代码,都是从该区域读取;
该区域是只读的,防止意外修改导致程序出错,可以对各种数据修改,但程序本身不可修改。
全局区:
存放全局变量、静态变量;(在全局区内部,这两个类型的变量地址距离较近,与常量区较远)
全局区还包括了常量区,字符串常量和全局常量也会存放于此(不包含局部常量);
全局区空间是在程序结束时由操作系统释放

#include <iostream>
using namespace std;
int g_a = 10; //全局变量
const int g_c_a = 10; //全局常量
int main(){
	int a = 10; //局部变量
	static int b = 10; //静态变量;
	const int c_a = 10; //局部常量,不放在全局区,在栈区
	
	cout<<(int)&"abc"<<endl; //字符串常量,也是字面量,存放在全局区
}

2,程序运行中:
栈区:由编译器自动开辟释放,不要在函数内返回局部变量和形参(因为局部变量和形参放在栈区);

int* func(int b){
	b = 100;
	int a = 10;
	return &a;
}
int main(){
	int a = 20;
	int* p = func(a);
	cout<< *p <<endl; //由于函数结束,内存释放,所以大概率得不到10,但有些编译器做了特殊优化,可能会暂时保留,有可能拿到一次,第二次会失效
	cout<< *p <<endl; //这次必然失败
}

另外与Java不同的时,C++栈区可以存放任何类型的值,各种自定义的类型,只要不使用new关键字在堆区开辟空间,就是存放在栈区,所以经常看到以下这种情况:后面没使用new关键字,看起来仅仅只是一个声明(在Java中是这样的),但其实就已经开辟了空间,只不过是存放在栈区,在函数结束后由系统自动回收。

Person p;  //Person为自定义类型class
int arr[10];

堆区:由new关键字开辟的数据存放在堆区,由程序员手动管理(释放使用delete关键字;而Java是自动释放,一般都是没有指针指向这块区域时),如果不释放,会在程序结束时由系统释放;另外由于返回的是指针,开辟数组时就只能使用指针来接收了:

int arr[10] = new int[10]; //返回的是指针,但左值是数组,报错
int* array = new int[10];  //标准写法

另外还有跟Java不一样的是,堆区可以存放普通的值,如int、double型的值;Java中只能存放引用类型对象,另外开辟后返回的是数据在堆区中的地址,这点倒是与Java一样。

#include <iostream>
using namespace std;
int* func(){
	int* p = new int(10); //存放在堆区,初始化为10,函数结束,堆区不会被释放,因此还是可以访问
						  //而指针p存放在栈区,函数结束释放
	return p;  //虽然函数结束指针p释放,但返回的是堆区的地址,因此在函数外依然可以访问到
}
int main(){
	int* p = func();
	cout<< *p << endl;  //10
	cout<< *p << endl;  //10
	cout<< *p << endl;  //10 堆区内存没释放,因此可以访问到

	delete p;  //手动释放该区间
	cout<< *p <<endl; //报错,因为该空间已被释放,此时程序员已经没有权限再去访问、操作该空间

	int* arr = new int[10]; //在堆区开辟一个数组,数组大小为10
	delete[] arr;   //释放这个数组空间,注意要加上[],不加上[]只会释放该数组首个元素空间
}

二,引用:

1,引用就是给一个变量起个别名,本质上指向的是同一个空间;

int a = 10;
int &b = a;
b = 20;
cout << a << endl; //20

2,引用必须初始化,并且一旦初始化就不能更改

int a = 10;
int c = 20;
//int &b; //报错
//int &b = 20; //报错,初始化错误,右值必须是个变量
int &b = a;
b = c; //本意是把c的值赋给b的空间,此后a的值变为20,并不是让引用b指向c的空间,引用b依然指向a的空间

3,引用做函数参数,跟地址传递的效果是一样的,但代码更加精简

void func(int &a,int&b){  //以引用的方式来接int实参,效力同指针
	int temp = a;
	a = b;
	b = temp;
}
int main(){
	int a = 10;
	int b = 20;
	func(a,b);  //函数执行之后,a = 20,b = 10;
	cout<< a <<endl;  //20
	cout<< b <<endl;  //10
	return 1;
}

4,不要返回局部变量引用,因为保存在栈区,函数结束空间释放,可能第一次访问还可以访问到(编译器保留),第二次就不会访问到了

5,函数调用可以作为左值

int& func(){
	static int a = 10;
	return a;   //因为静态变量保存在全局区,以引用方式返回,因此后续还可以访问到
}
int main(){
	int &temp = func();
	int temp2 = func(); //这是一个新的局部值,其存储地址跟静态变量a不同
	cout<<temp<<endl;  //10
	cout<<temp<<endl;  //10
	cout<<temp<<endl;  //10
	cout<<temp2<<endl; //10
	func() = 20;
	cout<<temp<<endl;  //20
	cout<<temp<<endl;  //20 
	cout<<temp2<<endl; //10 因为temp2是新值,跟a实际上没啥关系,对a的修改不会影响temp2
	/*因为返回的是静态变量a的引用,因此temp和func()实际都是a的引用,func() = 20这句实际是修改a的值为20,所以后续打印temp都是新值20*/
}

6,引用的实质就是一个常量指针

int a = 10;
int &b = a; /*编译器自动转换为:int* const b = &a;因此b不可以再指向其他变量。后续再使用b时,编译器自动解引用  */

后续使用时编译器自动解引用!这也就解释了为什么既然引用的本质是一个指针,访问数据时为什么用(.)而不是(->),包括自定义struct或class,使用访问成员变量都是使用(.)
7,常量引用
常量引用实质就是 const int* const b = 10; 无论是指向的地址,还是地址内的数据都不可以更改,常常用来作为函数的参数,来防止函数对实参的误修改。

int &a = 10; //报错,因为引用本质是指针常量,不可指向其他区域,但一旦确定指向,里面的值就可以修改,10是字面量放在全局区里面的常量区,不可修改,因此报错。
const int &b = 10; //不会报错,编译器自动执行 int temp = 10; const int &b = temp;自动创建一个临时变量,然后让b引用它

注意:
1,指针或者引用作为函数参数不一定可以节省空间,如果参数是结构体或者数组还行,如果是int型这种简单类型可能不会节省空间,因为指针也要占空间。
2,一个长久的疑惑点:操作符(*)与(&)在定义和使用的时候含义是不同的:

int* p = a; //*表示这是一个指针类型
*p = 20;    //*表示解地址,表示一个动作,将这块地址内的数据修改为20

int& q = a; //&表示这是一个引用
int* p = &q; //&表示取地址,一个动作

三、函数高级

1,函数参数可以有默认值:一旦有了默认值,那么在调用时就可以不传,如果参数全都有默认值,甚至可以一个参数都不传。

int func(int a,int b = 20,int c = 30){
	return a + b + c;
}
int main(){
	int result = func(10,10,10); //传入了值,用实参
	cout<< result <<endl;  //30
	cout<< func(100)<<endl; //150,b和c没有传值,用默认值 
	cout<< func(100,200)<<endl; //330,c没有传值,用默认值 
}

注意事项:
(1)如果某个参数有了默认值,那么这个参数之后的每个参数都要有默认值;
(2)函数的声明和实现只能有一处有默认值;因为在执行时有歧义,编译器不知道按哪套默认值来执行;

int func(int a = 10,int b = 20); //声明 
int func(int a = 100,int b = 200){  //实现,出现歧义,代码跑起来会报错
	return a + b;
}

2,函数可以有占位参数:但目前还用不到,随着学习的深入可能会用到,这个占位参数也可以有默认值,有默认值之后就不必再传实参了

void func(int a, int){
	cout<<"函数执行了"<<endl;
}
void func2(int a, int = 10){
	cout<<"函数2执行了"<<endl;
}
int main(){
	func(10); //报错,有个占位参数,必须也当成参数传值
	func(10,20); //正确的

	func(10); //正确的,因为占位参数有默认值
}

3,函数重载:与Java类似,必须在同一个作用域下,函数名相同,其参数列表不同(类型、数量、顺序不同),返回值不能作为函数重载的依据;
函数重载注意事项:
(1)引用作为重载的条件:

//这两个函数可以构成重载
int main(){
	int a = 10;
	func(a);  //会走第一个,因为实参a是变量可以修改,第二个函数不许修改,所以会走第一个。(两个都可以走得通但偏向第一个)
	func(10); //会走第二个,因为引用必须指向一块可以修改的区域,10是常量存在于全局区里的常量区,走第一个会不合法,只能走第二个
}
void func(int& a);
void func(const int& a);  

(2)重载碰到默认参数:

//在执行调用时,func(10);两个函数都可以走通,出现了歧义,会报错
void func(int a, int b = 10);
void func(int a);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值