C++入门--引用的本质、const限定符、this指针、nullpter

引用

引用的概念

引用不是新定义一个变量,而是给已存在变量去一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

引用定义:类型& 引用变量名(对象名) = 引用实体;

int main(){
	int a=10;
	int &b=a;
	cout<<"a的地址:"<<&a<<endl;
	cout<<"b的地址:"<<&&b<<endl;
	
	return 0;
}

运行结果图:

在这里插入图片描述
内存分布图:

在这里插入图片描述


注意:引用类型必须和引用实体是同种类型。

引用的本质

在语法概念上引用就是一个别名,没有独立的空间,和其引用实体共用同一块空间,看上图的运行结果。但是在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

int main()
{
	int a = 10;
	
	int& ra = a;
	ra = 20;
	
	int* pa = &a;
	*pa = 20;
	
	return 0;
}

我们来看一下引用和指针的汇编比较:

在这里插入图片描述
引用的本质在C++内部实现是一个常量指针。

Type &ref=val; ==> Type *const ref=&val;
int a=10;
int &var=a;==> int *const var=&a;

C++编译器在编译过程中使用常量指针作为引用的内部实现,因此引用所占的空间大小与指针相同,只是这个过程是编译器内部实现,用户不可见。

引用的特性

1、引用在定义时必须初始化,因为引用的本质是一个常量指针,所以必须初始化。

2、引用一旦初始化,不能再引用其他对象。

3、一个变量可以有多个引用。

4、不能有NULL引用。必须确保引用是和一块合法的存储单元关联。

// 使用引用注意事项
void test02(){
	//1) 引用必须初始化
	//int& ref; //报错:必须初始化引用
	//2) 引用一旦初始化,不能改变引用
	int a = 10;
	int b = 20;
	int& ref = a;
	ref = b; //不能改变引用
	//3) 不能对数组建立引用
	int arr[10];
	//int& ref3[10] = arr;
	
	//1. 建立数组引用方法一
	typedef int ArrRef[10];
	int arr[10];
	ArrRef& aRef = arr;
	for (int i = 0; i < 10;i ++){
		aRef[i] = i+1;
	}
	for (int i = 0; i < 10;i++){
		cout << arr[i] << " ";
	}
	cout << endl;
	//2. 建立数组引用方法二
	int(&f)[10] = arr;
	for (int i = 0; i < 10; i++){
		f[i] = i+10;
	}
	for (int i = 0; i < 10; i++){
		cout << arr[i] << " ";
	}
	cout << endl;
}

引用的使用场景

做参数

void swap(int &x,int &y){
	int temp=x;
	x=y;
	y=temp;
}

做返回值

int &add(int a,int b){
	int c=a+b;
	return c;
}
int &test02(){
	static int a=10;
	cout<<"static int a:"<<a<<endl;
	return a;
}

int main(){
	int &ret=add(1,2);//ret=3;
	add(3,4);//再次运行时,建立和第一次相同的栈帧,c=7,由于返回的引用,所以ret的值被修改为了7。
	cout<<"add(1,2) is :"<<ret<<endl;//ret=7; 编译器做了优化
	cout<<"ret is :"<<ret<<endl;//ret指向一个非法空间地址
	test02();
	test02()=100;//当返回引用时可以做左值,又由于a的作用域是本文件,所以可以直接修改a的值。
	test02();
	return 0;
}

运行结果图:
在这里插入图片描述

注意:不要返回非静态局部变量引用,因为出了函数作用域,临时对象占用的空间也就随之释放了,所以ret都指向了不可再用的内存空间。当引用做返回值时可以做左值使用,可以改变引用所绑定那个对象的值。

传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回值期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝,因此用值作为参数或者返回值类型,效率是非常低下的尤其是当参数或者返回值类型非常大时,效率就更低了。但是以引用作为参数或者返回值类型时,那么函数直接传递实参或者将变量本身直接返回,所以没有了临时拷贝的过程,效率就提高了。

C++测试代码:


#include<ctime>
struct A {
	int a[10000];
};
void test01(A a) {

}

void test02(A &a) {

}

int main() {
	A a;
	//以值作为函数参数
	size_t begin1 = clock();
	for (int i = 0; i < 10000; i++) {
		test01(a);
	}
	size_t end1 = clock();
	
	//以引用作为函数参数
	size_t begin2 = clock();
	for (int i = 0; i < 10000; i++) {
		test02(a);
	}
	size_t end2 = clock();
	
	//分别计算两个函数运行结束后的时间
	cout << "test01(A)-time:" << end1 - begin1 << endl;
	cout << "test02(A&)-time:" << end2 - begin2 << endl;
	return 0;
}

运行结果图:
在这里插入图片描述

指针和引用的不同点

1、引用在定义时必须初始化,指针没有要求。

2、引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。

3、没有NULL引用,但有NULL指针。

4、在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)

5、引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。

6、有多级指针,但是没有多级引用。

7、访问实体方式不同,指针需要显式解引用,引用编译器自己处理。

8、引用比指针使用起来相对更安全。


const限定符

我们希望定义一种变量,它的值不能修改,我们可以只用const关键字对变量的类型加以限定。const一旦创建后其值就不能再改变,因此const对象必须初始化。

const int i=get_size();		//正确:运行时初始化
const int j=42;				//正确:编译时初始化
const int k;				//错误:k是一个未经初始化的常量

C和C++区别

C语言代码:

void test() {
	const int m_b = 20;
	//int arr[m_b];//伪常量不可以初始化数组
	int* p = (int*)&m_b;

	*p = 200;

	printf("*p = %d\n",*p);

	printf("m_b = %d\n", m_b);
	
}
int main() {
	test();
}

运行结果:

在这里插入图片描述
C++语言代码:

void test() {
	const int m_b = 20;
	int arr[m_b];//可以初始化数组
	int* p = (int*)&m_b;

	*p = 200;

	cout << "*p = " << *p << endl;;

	cout << "m_b = " << m_b << endl;

}
int main() {
	test();
}

运行结果:
在这里插入图片描述
解释:

在这里插入图片描述

C语言中const默认外部链接

//test.c
const int a=20;
//main.c

int main(){

	extern const int a;
	printf("a = %d",a);
	return 0;
	
}

C++语言中const默认内部链接

//test.cpp
const int a=20; //extern int a=20;默认是内部链接,可以使用extern提高作用域
//main.cpp

int main(){

	extern const int a;
	cout<<"a = "<<a<<endl;
	return 0;
}

// 错误	LNK1120	1 个无法解析的外部命令	

如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。

常引用

我们可以把引用绑定到const对象上,我们称之为对常量的引用,不能用作对修改它所绑定的对象。

void test(){
	const int ci=1024;
	const int &r1=ci;	//正确:引用及其对应的对象都是常量
	
	r1=42;	//错误:r1是对常量的引用,不能修改绑定的对象
	int &r2=ci;	//错误:试图让一个额非常量引用指向一个常量对象

	// int& b = 10; // 该语句编译时会出错,b不是常量
	const int& b = 10;
	
	double d = 12.34;
	//int& rd = d; // 该语句编译时会出错,类型不同
	const int& rd = d;
}

解释如下:

在这里插入图片描述

指针和const

指向常量的指针

指向常量的指针:不能用于改变其所指对象的值。要想存放常量的地址,只能使用指向常量的指针。

const double pi=3.14;	//pi是个常量,它的值不能改变
double *ptr=&pi;		//错误:ptr是一个普通的指针,如果这样合法那么久意味着可以通过ptr来改变pi的值。
const double *cptr=&pi;	//正确:cptr可以指向一个双精度常量
*cptr=42;				//错误:不能给*cptr赋值

常量指针

常量指针:必须初始化,而且一旦初始化完成,则指针的值就不能再改变了。指针将一直指向初始化那个对象的地址。

int errNum=0;
int *const curErr=&errNum;//curErr将一直指向errNum
const double pi=3.14;
const double *const pip=&pi;//pip将一直指向pi,而且*pip将不能任意赋值,及不能通过*pip改变pi的值

总结:const 离谁近谁的值就不能改变。

this指针

this指针的引入

class Date {
public:
	void Display() {
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	void SetDate(int year,int month,int day) {
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main() {
	Date d1, d2;

	d1.SetDate(2021, 2, 6);
	d2.SetDate(2021, 2, 7);
	d1.Display();
	d2.Display();
	return 0;
}

Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当s1调用SetDate函数时,该函数是如何知道应该设置s1对象,而不是设置s2对象呢?我们知道C++的数据和函数是分开存储的,并且每一份非内联成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。

在这里插入图片描述

问题:这一块代码是如何知道哪一个对象调用的自己呢?

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

注意:静态成员函数内部没有this指针,静态成员函数不能操作非静态成员函数。

this指针的特性

1、由于this指针一直指向当前对象,所以this指针的类型是: * const 。

2、只能在非静态成员函数中使用。

3、this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。

4、this指针是成员函数隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。

void Display(){
	cout<<_year<<endl;
}
//等价于
void Display(Date * const this){
	cout<<this->_year<<endl;
}
//下面程序会崩吗?在哪里崩溃

class A
{
public:
	void PrintA()
	{
		cout<<_a<<endl;
	}
	void Show()
	{
		cout<<"Show()"<<endl;
	}
private:
	int _a;
};
int main()
{
	A* p = NULL;
	p->PrintA();
	p->Show();
}

运行结果:
在这里插入图片描述


分析:虽然p是空指针,当我们在调用 p->Show()的时候,Show的代码是存储在代码区中,而且里面并没有使用this指针,所以能运行成功。但是当调用p->PrintA() 时, cout<<_a<<endl;==>cout<< this->_a<<endl;又因为this ==NULL;所以会报读取访问权限冲突。

const对象和const成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数的隐含的this指针this指针的类型为 const Type * const this。const修饰this指针指向的内存区域,成员函数体内不可以修改本类中的任何普通成员变量,当成员变量类型符前用mutable修饰时例外。

const修饰成员函数

class Person {
public:
	Person() {
		this->mAge = 0;
		this->mID = 0;
	}
	//在函数括号后面加上const,修饰成员变量不可修改,除了mutable变量,
	//	伪函数:void someOperate(const Person *const this)const 
	void someOperate()const {
		//this->mAge = 200; mAge不可修改
		this->mID = 20;
	}
	void ShowPerson() {
		cout << "ID:" << mID << " mAge:" << mAge << endl;
	}
private:
	int mAge;
	mutable int mID;
};

1、const对象只能调用const的成员函数。

2、非const对象可以调用const成员函数。

3、const成员函数内不能调用其它的非const成员函数。

nullptr

NULL来自C语言,一般由宏定义实现,而nullptr则是C++11的新增关键字。C语言中,NULL被定义为(void*)0,而在C++语言中,NULL则被定义为整数。编译器一般对其实际定义如下:

#ifdef __cplusplus
#define NULLL 0
#else
#define NULL ((void*)0)
#endif

在C++中指针必须有明确的类型定义。但是将NULL定义为0带来的另一个问题是无法与整数0区分。因为C++中允许有函数重载,所以可以看看一下函数:

void f(int)
{
	cout<<"f(int)"<<endl;
}
void f(int*)
{
	cout<<"f(int*)"<<endl;
}
int main()
{
	f(0);
	f(NULL);
	f((int*)NULL);
	return 0;
}
//输出结果:	f(int)
//			f(int)
//			f(int*)

那么在传入NULL参数时,会把NULL当作整数0来看。如果我们想调用参数是指针的函数,该怎么办?nullptr在C++11被引入用于解决这一问题,nullptr可以明确区分整数和指针类型,能够根据自动转换成相应的指针类型,但不会被转换为任何整数,所以不会造成参数传递错误。

本片文章的nullptr部分转载自公众号:拓扑阿秀。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值