引用
引用的概念
引用不是新定义一个变量,而是给已存在变量去一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
引用定义:类型& 引用变量名(对象名) = 引用实体;
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=π //错误:ptr是一个普通的指针,如果这样合法那么久意味着可以通过ptr来改变pi的值。
const double *cptr=π //正确:cptr可以指向一个双精度常量
*cptr=42; //错误:不能给*cptr赋值
常量指针
常量指针:必须初始化,而且一旦初始化完成,则指针的值就不能再改变了。指针将一直指向初始化那个对象的地址。
int errNum=0;
int *const curErr=&errNum;//curErr将一直指向errNum
const double pi=3.14;
const double *const pip=π//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部分转载自公众号:拓扑阿秀。