目录
7、引用:
7.1、引用的概念:
7.2、引用特性:
7.3 、常引用:
7.4、引用的使用场景 :
7.4.1、引用做参数(形参):
7.4.2、输出型参数:
7.4.3、引用做返回类型:
7.5、传值,传址,传引用,三者效率的比较 :
7.5.1、传值传参,传址传参,传引用传参,三者效率比较:
7.5.2、传值返回,传址返回,传引用返回,三者效率比较:
7.6、引用和指针的区别 :
8、内联函数 :
8.1、概念:
8.2、特性:
8.3、关联面试题:
9、auto关键字(C++11):
9.1、auto简介:
9.2、auto的使用细则:
9.3、auto不能推导的场景:
10、基于范围的for循环(C++11) :
10.1、范围for的语法:
10.2、范围for的使用条件:
11、指针空值(空指针)nullptr (C++11):
11.1、C++98中的指针空值(空指针):
类型& 引用变量名(对象名) = 引用实体;
例如:
int a = 5;
int& b = a;//b叫做a的引用,或者称b是a的别名、
//此时,b并不是一个新的变量,它只是变量a的一个别名,所谓的b的地址,在本质上指的就是a的地址,故,a和b的地址是相同的,都指向了同一块内存空间,变量a可以具有无数个别名,但是不管其有多少个别名,在本质上,地址和所指向的内存空间均只有一份,就是变量a对应的地址和内存空间、
int& c = a;
int& d = b;//可以对a的别名b取别名为d、
//b、c、d这三者均不是变量,但都是变量a的别名,都可以用来修改变量a的值,此时,a,b,c,d的地址都是一样的、
图示:
通过监视可以看到:
a、b、c、d 这四个标识符的值都是一样的,并且对其进行取地址后其地址也是一样的、
那么引用的用处在哪呢?
#define _CRT_SECURE_NO_WARNINGS 1
#include
using namespace std;
//形参是实参的别名、
void Swap(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
int main()
{
int x = 1,y = 2;
cout << x << " " << y << endl;
Swap(x, y);
cout << x<<" "<< y << endl;
return 0;
}
运行结果为:
此处可以看到,使用引用后,不需要传参变量x和y的地址,不需要使用指针就可以通过改变形参的值从而改变实参的值,由此可知,使用引用和
使用传址调用可以达到相同的效果、
注意:引用也可以给指针变量取别名、
//C++脱离不开指针,像链表,二叉树这些结构必须使用指针,但Java能够脱离指针、
//Java的引用可以改变指向,C++的引用不能改变指向,所以Java可以直接使用引用就可以实现各种数据结构、
//所谓C++的引用不能改变指向,即指,当引用一个实体后,不能再引用其他实体,而Java却可以,是因为,Java的引用可以改变指向、
//C++和Java可以引用任何类型,除了void、
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include
using namespace std;
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode,*PLTNode; //此时PLTNode是struct ListNode* 的重定义,而LTNode又是struct ListNode的重定义,故,PLTNode === LTNode*、
void LTPushBack_C(LTNode** pphead, int x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
assert(newnode);
if (*pphead == NULL)
{
*pphead = newnode;
}
}
//形参是实参的别名、
void LTPushBack_CPP_1(LTNode*& phead, int x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
assert(newnode);
if (phead == NULL)
{
phead = newnode;
}
}
//形参是实参的别名、
void LTPushBack_CPP_2(PLTNode& phead, int x)
{
PLTNode newnode = (PLTNode)malloc(sizeof(LTNode));
assert(newnode);
if (phead == NULL)
{
phead = newnode;
}
}
int main()
{
LTNode* plist = NULL; //PLTNode plist = NULL;
LTPushBack_C(&plist, 1);//传址调用、
LTPushBack_CPP_1(plist, 1);//引用、
LTPushBack_CPP_2(plist, 1);//引用、
return 0;
}
或者:
在调用函数中,pa指针变量的值被修改成了空指针、
可以定义引用的引用,即,可以给别名取别名,但是,书写格式要正确,比如:
//正确示例:
int a = 10;
int& ia = a;
int& iia = ia;
//错误示例:
int a = 10;
int& ia = a;
int&& iia = ia; //此处的 iia 的定义就是非法的、
int &a; //该条语句编译时会出错
在初始化变量时,初始值会被拷贝到新建的对象中,然而定义引用时,程序会把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用,一
旦初始化完成,引用将和它的初始值对象一直绑定在一起,因为无法使得引用重新绑定到另一个对象,所以,引用必须在定义的时候进行初始
化、
int a = 10;
int& ia = a;
int b = 20;
int& ib = b;
ib = a;
ib = ia;
上面进行的最后两行操作,比如 ib=a 和 ib=ia 都是进行的赋值操作,而不是对引用的更改、
当引用作为左值时,是被指向的那个对象本身的内存空间,比如在上面的例子中,就是指代的标识符 ib 指向的那个整型变量b的内存空间、
当引用作为右值时,是被指向的那个对象本身的内存空间所存储的值,在上面的例子中,ia在作为右值时就是a这个整型变量所指向的内存空间中存储的值10、
一、
const也可以用来修饰变量,如果在变量 i 的左边加上const,不管是 const int i =20;还是 int const i =20;,这都是把const放在了变量 i 的左
边,那么const修饰的就是变量 i ,不存在int i const =20;这种形式,我们一般来说,都把const放在类型的左边,即:const int i =20,const修
饰变量,该变量的值就不能被修改,所以如果想改变 i 的值,是不行的,此时的 i 是常属性的,不可以被改变、
下面的操作是非法的:
const int x = 20; //只读、
int& y = x; //可读可写、
//权限放大,错误示例,编译时会报错、
修改代码即可成功编译,如下所示:
const int x = 20; //只读、
const int& y = x; //只读,一般都把const放在类型的左边、
//权限不变,不会报错,正确示例、
//此时,标识符y是整型变量x的引用(别名),但是不可以修改标识符y的值、
再如下所示,也可以成功编译:
int c = 30;//可读可写、
const int& d = c;//只读,一般都把const放在类型的左边、
//权限变小,可成功编译,正确示例、
cout<
由上述例子可知,取别名的原则或者说是引用的原则即为:引用相对于原变量的读写权限,只能缩小或者不变,但是不能放大、
二、
不可以直接对常量取别名,如下所示:
int a = 10;
int& b = a; //正确、
int& c = 20; //错误、
修改代码即可成功编译,如下所示:
int a = 10;
int& b = a; //正确、
const int& c = 20; //正确、
//此时,20是常数,具有常属性,不能被修改,只能被读取,若不加const的话,则属于只读变为可读可写,则权限变大,编译失败,所以要加上const,此时则为:只读变为只读,权限不变,编译成功、
//此时,标识符c是常数20的引用(别名),标识符c不可以被修改,只能被读取,更不能通过修改标识符c的值来改变常数20的值、
三、
double d = 2.2;
double& e = d; //正确、
但是,若为下图所示,则是非法的:
double d = 2.2;
int& e = d; //错误,引用类型和引用实体类型不同、
修改代码即可成功编译,如下所示:
double d = 2.2;
const int& e = d; //正确、
//虽然会有提示为:"初始化": 从"double"转换到"const int",可能丢失数据,但也能够成功编译、
要想知道上述代码能够成功编译的原因,我们首先要了解一下什么是隐式类型转换:
在C语言中,存在隐式类型转换,而C++兼容C语言,故,也会存在隐式类型转换,隐式类型转换主要包括相似类型,即,整型家族之间的转换,
例如,互相赋值,比如,char,short,int,long,long long类型之间的转换,大给小会截断,小给大会提升,其次还包括浮点型与整型之间的转
换,浮点型和整型的存储机制不一样,所以不会是简单的截断和提升,当浮点型转整型时,则是把整型部分数据取出来赋过去,把浮点型部分数
据舍弃,当整型转浮点型时,则要把整数部分数据取出来赋过去,再把浮点部分数据用0填充、
注意:标识符e不是浮点型变量d的别名,而是在隐式类型转换过程中产生的临时变量的别名, 所以标识符e和d的值,以及他们的地址都是不相
同的,标识符e的生命周期和临时变量的生命周期相同、
如下所示:
double d = 2.2;
int f = d;
当执行 int f =d 时,即,进行隐式类型转换时,不是直接进行赋值的,由于是浮点型与整型之间的转换,故也不是直接截断的,而是,编译器会
把浮点型变量d的整数部分取出,放在一个大小为4byte的临时变量中,然后临时变量再把其中的值赋给整型变量f,即,隐式类型转换时,会在过
程中产生临时变量,而临时变量具有常属性,不能被修改,只能被读取,所以要明白,当引用类型和引用实体类型不同时,不能进行引用的本质
是因为,在执行引用这条代码时,会产生临时变量,存储位置一般是寄存器,而临时变量是常属性,若不加const的话,则引用的属性是可读可写
的,所以不能直接进行引用的本质则是属性不同,而不是引用类型和引用实体类型不同,若在引用前面加上const,则编译是可以成功的,即使引
用类型和引用实体类型不同,也是可以的,若不加const的话,则属于读写权限变大,只读变可读可写, 编译失败,当加上const后,权限不变,
只读变只读,就可以成功编译、
注意:只有当进行引用(取别名)或者使用指针的时候,才会考虑读写权限的问题,其他情况下,则不考虑读写权限的问题,因为只有使用引用和
指针时才会涉及到相互影响的问题,比如:int a =3,不涉及引用和指针操作,只是将3的二进制位拷贝到变量a所在的内存空间中,所以不存在
权限的放大与缩小的问题,因为a的改变不会改变3,只有指针和引用操作才会存在权限的放大与缩小的问题、
还有另外一种理解:
//注意:这种理解方法只适合目前这种情况,其他的情况并不能使用该理解方法去理解、
double d = 2.2;
const int& e = d; //正确、
//等价于const int& e = 2.2; 因为,2.2是常数,具有常属性,只读但不可以被修改,因为左边的类型是const int&,所以发生了隐式类型转换,变成了2, 2是常数,具有常属性,只读但不可以被修改,若不加const的话,则属于权限变大,编译错误,若加上const的话,则权限不变,可以编译成功、
//标识符left和right分别为变量a和b的别名,在调用函数Swap内部交换标识符left和right的值,就相当于是交换变量a和b的值,交换之后a的值位20,b的值为10、
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a,b);//实现了变量a的值和变量b的值的交换,交换之后a的值位20,b的值为10、
return 0;
}
意义:
1、使用引用操作代替指针操作,使得代码更加容易理解、
2、减少拷贝,提高效率,参考引用做返回类型中传引用返回的总结、
//引用操作还可以在 输出型参数中 进行使用,一般会出现在OJ题中,比如:
//C语言中只能写成:
int* preorderTraversal(struct TreeNode* root,int* returnSize)
{
}
//C++中除上面的C语言方法外,还可以写成:
int* preorderTraversal(struct TreeNode* root,int& returnSize)
{
}
首先先看下面的一段代码:
int Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
cout << Count() << endl;
cout << Count() << endl;
cout << Count() << endl;
return 0;
}
上面的输出结果是1,2,3,因为n是静态局部变量,存储在静态区,生命周期在整个程序运行期间始终存在,所以多次调用Count函数,使用的
都是同一个静态局部变量n,但是其作用域只在调用函数Count内部,且静态局部变量只初始化一次,所以只运行一次static int n = 0;
,所以
输出结果是1,2,3,具体请见博客:二叉树3,静态全局变量的作用域和生命周期都是整个工程、
下面看这段代码:
int Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret=Count();
return 0;
}
上述例子即为传值返回,在该过程中调用函数Count栈帧销毁之前会产生一个临时变量(寄存器或者是上一层函数栈帧中的一块内存空间或者是需
要的地方直接充当临时变量),当把返回值拷贝到临时变量中后,调用函数Count栈帧再进行销毁,如果该返回值所占内存空间较小的话,比如
4/8byte,那么临时变量就用寄存器进行替代,如上图所示,临时变量就用寄存器进行替代,调用函数Count的返回类型是int,其实本质上就是该
临时变量的类型为int,在传值返回过程中,把静态局部变量n中的值拷贝到临时变量中,然后临时变量中的值再拷贝到变量ret中,否则就不用寄
存器替代,比如返回的是一个结构体,该结构体所占内存空间较大,则会在调用函数Count的上一层栈帧,即,main函数栈帧中提前开好内存空
间,此时,在调用函数Count栈帧销毁之前把要返回的结构体拷贝到这个已经提前开辟好的内存空间中,然后再从该内存空间中把返回值拷贝到
所需要的地方、甚至有一些比较激进的编译器,会进行优化,这样就不在上一层函数栈帧中提前开辟好内存空间,而是在调用函数Count栈帧销
毁之前,把要返回的结构体直接拷贝到需要的地方,即,让需要的地方直接充当临时变量、
注意:无论是函数在进行传参还是函数在传值返回的时候,都会形成一个临时变量,如果所传的参数或返回值所占内存空间较小,那么临时变量
就用寄存器进行代替,否则就不用寄存器替代、
问:为什么在传值返回时要设计一个临时变量?
答:若上图中局部变量n不是静态局部变量的话,如果没有临时变量,在Count函数调用结束之后,函数栈帧就销毁了,局部变量n所在的内存空间就无法使用了,所以就无法将返回值进行返回,但是如果我们将局部变量n的值拷贝到一个临时变量中,这个临时变量可能是寄存器,也有可能是内存中的某一个空间(比如在调用Count函数之前提前在main函数中开辟好要存储返回值的空间),也可能是需要的地方直接充当临时变量,但无论如何,这个临时变量都已经不在Count函数的栈帧上了,所以我们可以在Count函数栈帧销毁之后依然可以将n的值传递回main函数中,在项目中,很少使用静态变量,可能会出现问题,比如一些线程安全的问题,全局变量也比较少用,也会出现一些线程安全的问题,否则当出现了线程安全问题后必须要加锁才可以,就会比较麻烦,局部变量的每个线程都是独享的,多个线程同时调用局部变量的时候不存在线程安全的问题,若按照上述代码来看的话,由于n是静态局部变量,所以其生命周期是整个工程,当从调用函数Count出来之后,静态局部变量n不会被销毁,理论上可以直接把n的值赋给变量ret,但,若n只是局部变量的话,从调用函数Count出来之后,局部变量n就会随着调用函数Count栈帧的销毁而销毁,不可以直接把局部变量n的值赋给变量ret,所以,综上所述,不管n是局部变量还是静态局部变量,当调用函数Count执行完毕后,都会产生一个临时变量,通过该临时变量来对变量ret进行赋值,调用函数Count栈帧的销毁,不会影响寄存器、
如上所示,传值返回的意义就是临时变量的类型是int类型,怎么证明上述传值返回过程中产生了临时变量呢?
如下所示:
int Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int& ret=Count(); //错误、
return 0;
}
当执行代码: int& ret=Count(); 时,此时是对临时变量进行引用,相当于是对临时变量取别名,而由于临时变量具有常属性,所以,此时相当
于是权限放大,编译错误,但加上const即可解决问题,因为这相当于是权限不变,编译成功,这也就证明了在上述传值返回过程中产生了临时变
量、
下面看这段代码:
int& Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret=Count();
return 0;
}
当引用做为函数的返回类型时,意义何在?
可以认为在调用函数Count栈帧销毁之前产生了一个临时标识符tmp,只不过该临时标识符tmp的类型为int&类型,则代码可理解为:int& tmp
=n;此时,标识符tmp则为变量n的别名,再把标识符tmp赋值给变量ret,此过程并未给标识符tmp开辟内存空间,把标识符tmp赋值给变量ret,
而标识符tmp又是变量n的别名,故相当于把变量n赋值给了变量ret、
总结:若是传值返回,则临时变量就是返回值的拷贝,临时变量的类型和调用函数返回类型一样,若是传引用返回,那么临时标识符则是被调用
函数栈帧中所要返回的变量的别名,并且在该过程中并未给临时标识符开辟内存空间、
怎么证明上图调用函数Count返回的是局部变量n的别名呢?
再看下面这段代码:
int& Count()
{
int n = 0;
n++;
return n;//返回的是n的别名
}
int main()
{
int& ret = Count();
return 0;
}
在调用函数Count栈帧销毁之前产生了一个临时标识符tmp,只不过该临时标识符tmp的类型为int&类型,则代码可理解为:int& tmp =n;此时,
假设标识符tmp则为变量n的别名,再对标识符tmp进行引用,即取别名,相当于是对别名取别名,并且能够编译成功,接下来,采用如下方式再
次进行验证:
int& Count()
{
int n = 0;
n++;
cout << "Count:" << &n << endl;
return n;
}
int main()
{
int& ret = Count();
cout << "main:" << &ret << endl;
return 0;
}
运行结果:
标识符ret和局部变量n的地址是一样的,说明标识符ret就是局部变量n的别名,并且也证明了,调用函数Count返回的是局部变量n的别名,即标
识符tmp就是局部变量n的别名,而标识符ret是标识符tmp的别名,而标识符tmp又是局部变量n的别名,故,标识符ret即为别名的别名、
传值返回和传引用返回有什么区别?
传值返回 :由于该过程中产生了临时变量,所以会存在拷贝、
传引用返回:该过程中没有产生临时变量,只是产生了临时标识符,则不存在拷贝,函数返回的直接就是变量的别名、
也就意味着,传引用返回相对于传值返回是在减少拷贝,在传引用返回过程中,产生的是临时标识符,不是临时变量,不需要寄存器等等,传引
用返回来的局部变量的别名,本质上返回来的就是该局部变量的地址、
问:上面例子中的Count栈帧已经销毁了,为什么还能在main函数中通过局部变量n的别名来访问到已经销毁的局部变量n呢?
答:函数栈帧的销毁只是我们不再拥有那段空间的使用权,那段空间依旧存在,我们依然可以通过引用或者指针去访问它,但是那段空间一旦被
其它的函数栈帧所覆盖,那么我们再次进行访问输出,得到的就是一个随机值,不管那段空间是否被其他函数的栈帧所覆盖,只要我们通过引用
或者指针去访问的话,这都属于非法访问,所以上面的代码理论上是错误的,原因就和存在野指针一样,越界访问也属于非法访问,非法访问不
一定会报错,但是这并不代码非法访问就是正确的,他应该是错误的、
int& Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int& ret = Count();
cout << ret << endl; // 1、
cout << ret << endl; // 随机值、
cout << ret << endl; // 随机值、
return 0;
}
运行结果:
在main函数中,执行完毕 int& ret = Count(); 后已经知道,标识符ret为局部变量n的别名,再执行 cout << ret << endl; 时,在栈帧的创建和销毁
中,先传参再建立调用函数的栈帧,所以先对参数进行操作,而此处的标识符ret作为参数,先要得到参数ret的值,由于,原来的调用函数Count
栈帧销毁后内存空间中的局部变量n的值还未被其他函数的栈帧覆盖,所以局部变量n的值没有发生改变,所以拿到第一次ret的值即为1,然后第
一个cout等其他相关函数开始建立栈帧,把原来销毁的调用函数Count的栈帧覆盖掉了,然后再进行打印,打印出第一个ret的值即为1,此时再进
行第一个cout等其他相关函数栈帧的销毁,但是当拿第二个ret的值之前,第一个cout等其他相关函数创建栈帧已经把原来的调用函数Count销毁
的栈帧覆盖了,所以当拿第二个ret的值,就会拿到随机值,然后第二个cout等其他相关函数开始建立栈帧,又把第一个销毁的cout等其他相关函
数的栈帧覆盖掉,然后再进行打印,打印出第二个ret的值即为随机值,此时再进行第二个cout等其他相关函数栈帧的销毁,但是当拿第三个ret的
值之前,第二个cout等其他相关函数创建栈帧已经把第一个cout等其他相关函数销毁的栈帧覆盖了,所以当拿第三个ret的值,就会拿到随机值,
然后第三个cout等其他相关函数开始建立栈帧,又把第二个销毁的cout等其他相关函数的栈帧覆盖掉,然后再进行打印,打印出第三个ret的值即
为随机值,此时再进行第三个cout等其他相关函数栈帧的销毁、
在有的编译器下,即使原来的调用函数Count栈帧销毁后内存空间中的局部变量n的值还未被其他函数的栈帧覆盖,但是该调用函数Count的栈帧
中的内容可能会被清除,也就是说,每当某个调用函数的栈帧结束后,编译器会自动清除该栈帧中的内容,这样的话,局部变量n的值就会发生
改变,那么就算拿第一次ret的值,也是随机值,经过测试可知,VS编译器下并未对该调用函数Count的栈帧中的内容进行清除,即,在VS编译
器下,经过测试可知,每当某个调用函数的栈帧结束后,编译器不会清除该栈帧中的内容、
注意: << 和 >> 也是函数调用,这叫做运算符重载,即:operator<<(ret) 和 operator>>(ret),ret在此作为参数,是函数调用就会存在函数栈帧
的创建和销毁,所以上述所讲的不仅仅只有cout函数的调用,还有<< 和 >> 函数的调用,严格来讲,上面不是cout的栈帧,因为,cout是一个对
象,不是一个函数,流插入会调用函数 cout.operatr<<(ret),流提取会调用函数 cout.operatr>>(ret),本质上应该是这两个函数的栈帧、
调用函数返回时,出了函数作用域,如果返回的对象还没有还给系统(常见的就是:static局部变量),即未被销毁,生命周期未结束,则可以使用传引用返回,也可以使用传值返回,只不过使用传引用返回比使用传值返回更加合适,,比如:减少拷贝,提高效率,修改返回值(运算符重载中讲解)等,还存在其他作用,在后面会进行阐述,如果已经还给操作系统了,则必须使用传值返回,不可以使用传引用返回,否则就可能会出现非法访问的问题,在类和对象中,有很多调用函数都需要使用传引用返回,当进行函数传参的时候,传值传参肯定没问题,除此之外,也可以直接使用传引用传参,也不会存在问题,即,当进行函数传参的时候,可以直接使用传引用传参和传值传参,都不会出现问题,不需要考虑这么多、
再看下面这段代码:
#include
using namespace std;
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add is :" << ret << endl; //7、
cout << "Add is :" << ret << endl; //随机值、
cout << "Add is :" << ret << endl; //随机值、
return 0;
}
由上述代码可知,标识符ret为局部变量c的别名,当执行完代码行 int& ret = Add(1,2); 后,第一次调用Add函数的栈帧就会销毁,虽然第一次调
用Add函数的栈帧销毁了,但是,此时,局部变量c的值为3仍保存在哪里,通过ret仍然能够找到那块空间,再执行 Add(3,4) 后,由于是调用的同
一个函数,则两次调用Add函数建立的栈帧是一样的,此时,局部变量c的值就会被替换成7,所以第一次打印出来ret的值应该是7,第二次,第
三次打印出来的结果都是随机值,具体原因在上面有总结、
再看下面这段代码:
#include
using namespace std;
int& Add(int a, int b)
{
static int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add is :" << ret << endl; //3、
cout << "Add is :" << ret << endl; //3、
cout << "Add is :" << ret << endl; //3、
return 0;
}
若定义为静态局部变量c的话,由上述代码可知,标识符ret为静态局部变量c的别名,当执行完代码行 int& ret = Add(1,2); 后,第一次调用Add函
数的栈帧就会销毁,但是由于c是静态局部变量,它不会随着调用函数Add的销毁而销毁,因为,静态变量存储在静态区上,而不是在栈区上,此
时,静态局部变量c的值为3,再执行 Add(3,4) 后,由于是调用的同一个函数,则两次调用Add函数建立的栈帧是一样的,但是,要知道,静态局
部变量的初始化只会执行一次,所以,第二次调用Add函数,即执行代码行 Add(3,4) 时,在调用函数内部,只执行 return c ,所以,此时的静态
局部变量c的值还是3,不会被替换成7,当执行第一次打印时,先去静态区拿到ret的值为3,然后在栈区上创建第一个cout等其他相关函数的栈
帧,打印出来的值为3,再进行第一个cout等其他相关函数的栈帧的销毁,当执行第二次打印时,还去静态区拿到ret的值为3,然后在栈区上创建
第二个cout等其他相关函数的栈帧,打印出来的值为3,再进行第二个cout等其他相关函数的栈帧的销毁,当执行第三次打印时,还去静态区拿到
ret的值为3,然后在栈区上创建第三个cout等其他相关函数的栈帧,打印出来的值为3,再进行第三个cout等其他相关函数的栈帧的销毁,静态变
量的生命周期是整个工程、
再看下面这段代码:
#include
using namespace std;
int& Add(int a, int b)
{
static int c = 0;
c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add is :" << ret << endl; //7、
cout << "Add is :" << ret << endl; //7、
cout << "Add is :" << ret << endl; //7、
return 0;
}
一定要知道,静态局部变量的定义和初始化所在的代码行只会执行一次、
#include
using namespace std;
#include
struct A{ int a[10000]; };
void TestFunc1(A b){}
void TestFunc2(A& b){}
void TestFunc3(A* b){}
void TestRefAndValueAndAddress()
{
//定义一个局部结构体变量、
A a;
// 以值作为函数参数 -> 传值传参、
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数 -> 传引用传参、
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
//传值传参和传引用传参的实参都是结构体变量,但是调用函数的形参不同、
// 以地址作为函数参数 -> 传址传参、
size_t begin3 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc3(&a); //实参是结构体变量的地址,地址的大小只和平台有关,4/8byte、
size_t end3 = clock();
// 分别计算三个函数运行结束后的时间
cout << "TestFunc1(A )-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
cout << "TestFunc3(A*)-time:" << end3 - begin3 << endl;}
int main()
{
TestRefAndValueAndAddress();
return 0;
}
运行结果为:
#include
using namespace std;
#include
struct A{ int a[10000]; };
//定义一个全局结构体变量、
A a;
//传值返回、
A TestFunc1() { return a; }
//传引用返回、
A& TestFunc2(){ return a; }
//传址返回、
A* TestFunc3(){ return &a; }
void TestReturnByRefOrValueOrAddress()
{
//以值作为函数的返回值类型 -> 传值返回、
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
//以引用作为函数的返回值类型 -> 传引用返回、
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
//以地址作为函数的返回值类型 -> 传址返回、
size_t begin3 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc3();
size_t end3 = clock();
//计算三个函数运算完成之后的时间、
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
cout << "TestFunc3 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValueOrAddress();
return 0;
}
int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a <
int main()
{
int a = 10;
//语法角度而言:ra是a的别名,没有额外开辟空间、
//底层角度而言,两者是一样的方式实现的,即汇编代码一样、
int& ra = a;
ra = 20;
//语法角度而言:pa是存储变量a地址的指针变量,为pa开辟了4/8byte空间、
//底层角度而言,两者是一样的方式实现的,即汇编代码一样、
int* pa = &a;
*pa = 20;
return 0;
}
double d =2.2; double& r=d;
cout<
5、引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小、
#include
using namespace std;
//当频繁调用像Add这样的短小函数(小于10行代码)时,就会造成效率浪费,因为,即使函数短小,也要建立栈帧,而在建立栈帧的过程中会做不少事情,比如:
//保存寄存器,压参数,压返回值等等操作,并且此时调用函数内部比较短小,这样的话,为了执行调用函数内部这几行代码,每次调用都要建立函数栈帧,则会
//有大量时间用于调用函数建立栈帧,用于执行调用函数内部这几行代码的时间却比较少,即,为了执行这短小的调用函数,却花费了大量时间用于函数栈帧的建立,
//这样的话就不太合适,那么如何操作才能避免像这种当频繁调用短小函数时,会花费大量时间用于调用函数栈帧的建立呢?
//一般情况下,当调用函数内部代码行大于等于10时(取决于编译器,经测试在VS下该数值为9),此时在每次调用时,建立函数栈帧所使用的时间相对于执行调用函数内部代码时间而言,不算浪费的严重,故就不考虑这种情况
//下来减少函数栈帧的建立,仍使用调用函数的形式即可、
//C语言使用 宏 来解决上述问题: 宏函数的好处是可以在调用的地方展开替换,不用建立栈帧、
//宏的错误写法:
//1、#define ADD(int x,int y) return x+y
//2、#define ADD(x,y) return x+y
//3、#define ADD(x,y) (x+y)
//4、#define ADD(x,y) x+y
//5、#define ADD(x,y) ((x)+(y));
//宏的正确写法:
#define ADD(x,y) ((x)+(y))
//验证自己写的宏是否正确,就去替换一下看是否能够得出想要的结果即可、
int Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
Add(1, 2);
Add(1, 2);
Add(1, 2);
Add(1, 2);
//....
Add(1, 2);
Add(1, 2);
return 0;
}
为什么C++中提出了内联函数概念呢?
由上述可知,在C语言中,为了解决上述出现的问题,可以使用宏进行操作,并且C++兼容C语言,所以在C++中也是可以使用宏来解决上述问题的,但
是,由上述可以看出来,正确书写宏比较麻烦并且困难,所以在C++中提出了内联函数的概念来代替宏用于解决上述出现的问题,所以内联函数出现的意
义就是用于解决宏函数晦涩难懂,容易书写错误的问题,除此之外,宏函数不支持调试,还不具有类型安全的检查功能等,但是,内联函数都很好的解决
了宏函数的缺点,还要注意,不管是普通的调用函数还是内联函数,当实参进行传参时,都是先进行计算再进行传参的,而宏函数则是直接进行替换的,
所以两者是存在一定的差别,所以普通的调用函数或者内联函数可以避免像宏函数那样由于优先级的问题从而导致出现错误的情况、
#include
using namespace std;
//内联函数能起到和宏函数一样的效果、
//inline:
//1、在默认的Debug版本下支持调试、
//2、书写简单,不容易出错、
inline int Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
Add(1, 2);
Add(1, 2);
Add(1, 2);
Add(1, 2);
//....
Add(1, 2);
Add(1, 2);
return 0;
}
要注意,内联函数在默认的Debug版本下面不会把该内联函数的定义展开到该内联函数的调用的地方,即,在汇编代码中存在call调用指令,这就是所谓
的不展开,这就是内联函数支持调试的原因、
当然,若对Debug版本进行优化一下,即,若进行下面的操作,即可使得内联函数在Debug版本下进行展开,具体展开过程不需要考虑,从而不能进行内
联函数的调试,具体操作如下所示:
经过上述操作, 即可使得内联函数在Debug版本下进行展开,即,在汇编代码中去除了call调用指令,并且不能再进行内联函数的调试,此时在调试中点
击F11,进不去调用函数内部,即,进不去内联函数内部,也就是所谓的不能进行内联函数的调试,除此之外,在release版本下,内联函数也会进行展
开,具体展开过程不需要考虑,从而不能进行内联函数的调试,由于该版本的优化程度过高,所以不能查看汇编代码,但是在该版本下,内联函数也会进
行展开,即,去除了call调用指令、
二、
宏的优缺点?
优点:
- 增强代码的复用性、
- 提高性能、
缺点:
- 不方便调试宏,(因为预编译阶段进行了替换)、
- 导致代码可读性差,可维护性差,容易误用、
- 没有类型安全的检查 、
C++有哪些技术替代宏?
- 常量定义换用const、
- 函数定义换用内联函数、
typeid().name
函数可以用来让编译器告知某个变量的类型是什么,包括自定义类型,该函数的返回类型是一个字符串、
#include
using namespace std;
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto f = &a;//&a是int*类型,编译器推导出变量f的类型是int*类型、
auto b = a; //a是int整型,编译器推导出变量b的类型是int整型、
auto c = 'a';//'a'是字符型,编译器推导出变量c的类型是char类型、
auto d = TestAuto();//TestAuto函数的返回类型是int型,编译器推导出变量d的类型是int整型、
const int e = 10;
//上行代码加上const之后,只会对下面的代码,并且等式右边为取地址的情况造成影响,若下面等式右边不是取地址,则上行代码中的const不起作用、
auto ie = &e;//&e的类型是 int const * 或者是 const int * 类型,编译器推导出变量ie的类型是:int const * 或者是 const int * 类型,即const在*的左边,修饰的是该指针指向的内容不能被改变,
//而该指针指向的内容就是变量e,所以正好和const int e =10,进行对应,但是编译器默认打印出来的是int const * 类型、
cout << typeid(f).name() << endl;//输出结果为int*、
cout << typeid(b).name() << endl;//输出结果为int、
cout << typeid(c).name() << endl;//输出结果为char、
cout << typeid(d).name() << endl;//输出结果为int、
cout << typeid(ie).name() << endl;//输出结果为int const *、
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化、
return 0;
}
//由于关键字auto是在C++11中修复的,所以,只有在11年及11年之后的编译器下才能成功编译、
注意:
使用关键字auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型,因此auto并非是一种"类型"的声明,而是一个类型声明时的"占位符",编译器在编译期会将关键字auto替换为变量实际的类型、
#include
using namespace std;
int main()
{
int a = 10;
auto* pa = &a;//指定变量pa为指针类型,由于等号右边是&a,即,这里属于auto声明指针类型,但使用auto*的情况,所以使用auto和auto*可以得到相同的结果,两者没有任何区别
//又因为当代码为 auto pa = &a; 时,此处的变量pa的类型是int*类型,所以上面的指针变量pa的类型即为int*类型、
auto* ppa = &pa;//指定变量ppa为指针类型,由于等号右边是&pa,即,这里属于auto声明指针类型,但使用auto*的情况,所以使用auto和auto*可以得到相同的结果,两者没有任何区别
//又因为当代码为 auto ppa = &pa; 时,此处的变量ppa的类型是int**类型,所以上面的指针变量ppa的类型即为int**类型、
auto& ia = a;//指定ia的类型为引用类型,由于变量a为int整型,故,此处的auto则为int类型,等价于 int& ia=a;
//auto* pa = a;//程序非法,因为指定了pa的类型是指针类型,所以等式右边只能是地址,否则就会报错、
cout << typeid(pa).name() << endl;//输出结果为int *、
cout << typeid(ppa).name() << endl;//输出结果为int **、
cout << typeid(ia).name() << endl;//输出结果为int、
return 0;
}
2、在同一行定义多个变量:
当在同一行定义多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个变量的类型进行推导,然后用推导出来的类
型定义其他变量、
auto a = 10, d = 3.14;//错误的使用方式、
auto i = 0, *p = &i;//正确的使用方式,i是int整型,p是int整型指针、
auto a = 10, b = 20;//正确的使用方式,因为经过编译器推导之后,程序会变成下面的定义方式
int a = 10, b = 20;
void TestAuto()
{
auto a = 1, b = 2; //正确、
auto c = 3, d = 4.0; //该行代码会编译失败,因为c和d的初始化表达式类型不同、
}
3、关键字auto在使用时会遵循隐式类型转换的规则,比如下面这样:
#include
using namespace std;
int main()
{
auto a = 3.14 + 5;//表达式3.14和5相加后的类型转换为double类型,值为8.14、
cout << typeid(a).name() << endl; //double、
return 0;
}
关键字auto的意义之一:当数据类型很长时,懒得写,可以让他自动推导、
//此处代码编译失败,关键字auto不能作为形参类型,因为编译器无法对a的实际类型进行推导,只有当该函数调用的时候才可以进行推导,但是在当该函数调用之前的编译时就会报错,和关键字auto定义变量时未初始化报的错误是一样的、
void TestAuto1(auto a)
{} //错误,否则函数重载就失去了意义、
//注意:关键字auto也不能作为缺省参数(形参)的类型,不管是全缺省还是半缺省参数,均不可以,例如:
void TestAuto2(auto a = 10) {} //错误、
//关键字auto也不可以做函数返回类型,此时编译失败、
auto TestAuto3(int a)
{} //错误、
//目前而言,关键字auto主要应用于:
//1、定义普通的变量、
//2、范围for中使用、
//目前而言,主要使用在这两个方面,除此之外也可以适用于其他的场景,具体的在以后会进行阐述、
//由上述可知,直接规定死,关键字auto不能作为形参的类型和调用函数的返回类型、
问:为什么会存在上述这种限制呢?
答:因为我们要把相应的一些函数接口暴露出来给用户的,用户需要知道如何使用,需要传什么类型的参数,需要什么类型的变量来接收返回值,而且函数的参数(形参)的类型和返回类型都是auto的话,函数的可读性就不太好了,并且也会加大编译器的工作量,在后面学习的模板中,就可以实现参数(形参)是任意类型的情况,此时可以不使用关键字auto来解决问题、
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6}; //错误、
}
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(int); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array) / sizeof(int); ++p)
cout << *p << endl;
}
但若在C++11及其以后的版本中遍历一个数组的话,除了上述方法之外,还可以使用下面的方式进行遍历,下面的方法只能在C++中使用,不能在C语言
中使用:
//范围for:
#include
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
for (auto e : array)
{
cout << e << " ";
}
cout << endl; //输出结果为:1 2 3 4 5
return 0;
}
//所有的数组均可按照该方法去遍历,不管数组的类型是什么,也不管数组的元素个数是多少个、
#include
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto e : array)
{
cout << e << " ";
}
cout << endl; //输出结果为:1 2 3 4 5 6 7 8 9 10
return 0;
}
//自动遍历,依次自动取数组中的内容,赋值给变量e,自动判断结束、
#include
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
//修改、
for (auto e : array)
{
e += 1;
}
//打印、
for (auto e : array)
{
cout<
经过测试发现,打印出来的结果仍是1 2 3 4 5 ,并没有修改数组中的值,这是为什么呢?
这是因为,范围for的原理是,依次自动取数组中的内容赋值给变量e,自动判断结束,那么此时,变量e值的改变并不会影响array数组中的内容,变量e中
的内容是数组array中的每个值的拷贝,虽然我们不能通过上述方法直接修改数组array中的值,但是可以通过如下操作达到通过上述方法修改数组array中
的值的目的,如下所示:
#include
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
//修改、
for (auto& e : array)
{
e += 1;
}
//在此不可使用把此处的array换成1 2 3 4 5的这种理解方法,此时应该是,可读可写变成可读可写,权限不变,可以编译成功、
//打印、
for (auto e : array)
{
cout<
此时,在修改的代码中,依次给数组array中的元素取别名,即,通过添加引用操作,即可达到目的,此处也是关键字auto的一种使用场景、
//打印、
for (int e : array)
{
cout<
范围for并没有规定变量e前面必须是关键字auto,也可以写成具体的类型,只不过该种写法直接写死了,没有关键字auto灵活,所以一般情况下,在写范
围for的时候,通常写成关键字auto的形式,除此之外,此处的变量名e也可以使用其他的字母来代替,可以自己随便定义,只不过,由于e是元素的英文
element的首字母,通常情况下习惯使用e,就像数组名通常使用arr或者array一样,如下所示:
//打印、
for (int x : array)
{
cout<
注意:范围for与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环、
这种写法的意义是什么?
其一是方便书写,其二是方便修改代码,例如我们向数组中添加了几个元素之后,使用这种方法就无需修改循环次数,且不用书写冗长的
sizeof(array)/sizeof(array[0])、
void TestFor(int array[]) //此处的array代表的是原数组首元素的地址、
{
for(auto& e : array) //此处的array代表的是原数组首元素的地址、
cout<< e <
函数传过去的实参只是一个地址(数组首元素的地址),编译器无法知道我们想向后遍历多大的范围,即for循环的范围不确定、
2、迭代的对象要实现++和==的操作,(关于迭代器这个问题,以后会讲,现在了解一下就可以了)、
nullptr是C++11中新增的关键字,用其来代表新的空指针,本质是((void*)0),在C语言中或者C++11之前(不包括C++11),
int* p1 = NULL; //方式1、
int* p2 = 0; //方式2,不进行探讨、
//在C++中,这两者是等价的、
但是,NULL实际是一个宏,并不是一个关键字,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
#include
using namespace std;
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);//int
f(NULL);//int
f((int*)NULL);//int*
f(nullptr);//int*
return 0;
}
若上述代码使用C语言编译器的话,会执行代码行 #define NULL ((void *)0) ,在预处理之后,NULL就会被替换成((void*)0),所以打印出来的分别为int和
int*,没有问题,不存在歧义,但是若使用C++编译器的话,则会执行代码行 #define NULL 0,在预处理之后,NULL就会被替换成0,而0当做自变量时,
会被看成一个整型常量,所以打印出来的是两个int,但是在意愿上,想要得到分别是int和int*,,所以,这就会存在歧义,这个问题在C++11(不包括
C++11)以前都存在,并没有什么好的解决方法,直到C++11中添加了关键字nullptr才解决了上述问题,所以,在C++11及其之后,为了避免再次出现上述
的歧义,当定义空指针时,常使用关键字nullptr来代替,这样就不会出现歧义了,此时也可以写成方式2,但方式2也不规范,所以最好使用关键字
nullptr,而在C语言中使用空指针时仍可以使用NULL,因为在C语言中不会出现歧义,也可以使用方式2,但是不规范,所以最好使用前者,在C++11之前
(不包括C++11)可以使用NULL,也可以使用方式2,但是都不规范,此时使用那个方式都可以,因为此时并没有发明关键字nullptr,所以在C++之前(不包
括C++11)都是存在问题的,但是由于我们现在使用的编译器一般情况下都兼容了C++11及其以后的等等最新的C++版本,所以,当我们在写C++代码时,
直接使用关键字nullptr就行,不再使用NULL,当然也可以使用方式2,但是并不规范,所以最好使用前者,而若是写C程序的话,还是继续使用NULL即
可,当然也可以使用方式2,但是并不规范,所以最好使用前者、
总结:若写C程序,空指针直接使用NULL,若写C++程序,空指针直接使用关键字nullptr、
注意:1、在使用nullptr表示指针空值时, 不需要包含头文件 ,因为nullptr是C++11作为新关键字引入的、2、在C++11中, sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同、3、为了提高代码的健壮性,在后续表示指针空值时建议最好使用 关键字nullptr、4、关键字nullptr的本质就是: ((void*)0)、