C++初阶(3)C++入门基础2
1. 引用
1.1 引用的概念和定义
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
引用 == 取别名(不是创建新变量,不会创建新的空间,共用同一块内存空间)
比如:水浒传中李逵,宋江叫"铁牛",江湖上人称"黑旋风";林冲,外号豹子头……
使用语法:
类型& 引用别名 = 引用对象;
C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如前面的<< 和 >>,这里引用也和取地址使用了同一个符号&。
注意从使用方法的角度区分就可以。
(吐槽一下,这个问题其实挺坑的,用更多符号反而更好,不容易混淆
祖师爷是认为避免用太多符号导致对符号不敏感了)
#include<iostream>
using namespace std;int main()
{int a = 0;// 引用:b、c是a的别名——不能说a是b的别名int& b = a;int& c = a;// 也可以给别名b取别名d,d相当于还是a的别名int& d = b;++d;//监视观察:a、b、c、d的值都加1,变成1了// 这里取地址我们看到四个标识符的地址是一样的——>说明没有创建新的变量(新的空间)cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;//可以认为a、b、c、d是同一块空间的不同名字return 0;
}
#include<iostream>
using namespace std;int main()
{int a = 0;int* pa = &a;//给指针取别名int*& rpa = pa;rpa = NULL//监视观察rpa和pa都被置零return 0;
}
1.2 引用的特性
• 引用在定义时必须初始化(必须指出是给谁取的别名)
• 一个变量可以有多个引用。(一个变量可以有多个别名)
• 引用一旦引用一个实体,再不能引用其他实体。
#include<iostream>
using namespace std;
int main()
{int a = 10;//int& ra;// 编译报错:“ra”: 必须初始化引⽤int& b = a;int c = 20;b = c; //b变成c的别名吗???// 这里并非让b引用c,因为C++语法规定,引用不能改变指向// 这里实际上是一个赋值——c赋值给b(a)//若写成int& b = c//报错:errorC2374:“b”:重定义;多次初始化cout << &a << endl;cout << &b << endl;cout << &c << endl;return 0;
}
引用和指针的区别:引用一旦确定了一个实体,就不能改变。
即:既没有空引用,也不能改变引用的指向。
可以给函数取别名。
函数名本质是函数的地址,给函数取别名(一般不会用到) ——相对于给指针取别名。
右值引用。
不报错的写法:
以后C++11会讲到。
1.3 引用的使用
• 引用在实践中主要场景是引用传参和引用返回。
作用:减少拷贝提高效率、改变引用对象时同时改变被引用对象。
• 引用传参跟指针传参功能是类似的,引用传参相对更方便一些。
• 引用返回值的场景相对比较复杂,我们在这里简单讲了一些场景,还有一些内容后续类和对象章节中会继续深入讲解。
• 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很大的区别的,除了用法,最大的点,C++引用定义后不能改变指向, Java的引用可以改变指向。
• 一些主要用C代码实现版本数据结构教材中,使用C++引用替代指针传参,目的是简化程序,避开复杂的指针,但是很多同学没学过引用,导致一头雾水。
#include<iostream>
using namespace std;
typedef struct SeqList
{int a[10]; int size;
}SLT;// ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,
//,避开复杂的指针,但是很多同学没学过引⽤,导致⼀头雾⽔。
void SeqPushBack(SLT& sl, int x)
{}typedef struct ListNode
{int val;struct ListNode* next;
}LTNode, *PNode;// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名
// 这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{PNode newnode = (PNode)malloc(sizeof(LTNode));newnode->val = x;newnode->next = NULL;if (phead == NULL){phead = newnode;}else{//...}
}int main()
{PNode plist = NULL;ListPushBack(plist, 1);return 0;
}
(1)引用传参
void Swap(int& rx, int& ry)
{int tmp = rx;rx = ry;ry = tmp;
}int main()
{int x = 0, y = 1;cout << x <<" " << y << endl;Swap(x, y);cout << x << " " << y << endl;return 0;
}
在C语言中:
- 传值传参,就无法完成交换——形参是实参的一份临时拷贝。
- 传址传参,就不能直接使用形参,要对形参解引用才能使用。
在C++中:
- 引用传参,直接使用形参,也能实现交换功能。
这就体现了引用的价值1——在一些场景下代替指针,避免了指针的复杂解引用操作。
//链表结点的类型
typedef struct ListNode
{int val;struct ListNode* next;
}LTNode;//C语言——链表尾插
void ListPushBack(LTNode** pphead, int x) //尾插可能会改变维护链表的表头,所以要传表头的地址
{assert(pphead)if(*pphead == NULL){//……return;}//……
}//C++———链表尾插
void ListPushBack(LTNode*& phead, int x) //直接传递维护链表的表头的引用
{if(phead == NULL){//……return;}//……
}
更简化的方式:
//链表结点的类型
typedef struct ListNode
{int val;struct ListNode* next;
}LTNode, *PNode;
//typedef struct ListNode* PNode//C++———链表尾插
void ListPushBack(PNode& phead, int x) //直接传递维护链表的表头的引用
{if(phead == NULL){//……return;}//……
}
要改变头结点的地址,就要传二级指针: LTNode**、LTNode*&、PNode& 。
引用传参(C++):对的传址/值传参(C语言)的优化。
typedef struct SeqList
{int val;//……
}SLT;//void SeqPushBack(struct SeqList& st, int x);
void SeqPushBack(SLT& st, int x);
这就体现了引用的价值2——避免拷贝,提高程序效率。
(2)引用返回
下面代码输出什么结果?为什么?
int& Add(int a, int b)
{int c = a + b;return c;
}
int main()
{int& ret = Add(1, 2);Add(3, 4);cout << "Add(1, 2) is :"<< ret <<endl;return 0;
}
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
引用的使用参考代码:
#include<iostream>
using namespace std;
typedef int STDataType;
typedef struct Stack
{STDataType* a;int top;int capacity;
}ST;void STInit(ST& rs, int n = 4)
{rs.a = (STDataType*)malloc(n * sizeof(STDataType));rs.top = 0;rs.capacity = n;
}// 栈顶
void STPush(ST& rs, STDataType x)
{assert(ps);// 满了, 扩容if (rs.top == rs.capacity){printf("扩容\n");int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2; STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}rs.a = tmp;rs.capacity = newcapacity;}rs.a[rs.top] = x;rs.top++;
}// int STTop(ST& rs)
int& STTop(ST& rs)
{assert(rs.top > 0);return rs.a[rs.top];
}int main()
{// 调用全局的ST st1;STInit(st1);STPush(st1, 1);STPush(st1, 2);cout << STTop(st1) << endl;STTop(st1) += 10;cout << STTop(st1) << endl;return 0;
}
1.4 常引用(const引用)
• 可以引用一个const对象,但是必须用const引用。
• const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
• 不需要注意的是类似:
//……int& rb = a*3; //errordouble d = 12.34; //errorint& rd = d; //error//……
这样一些场景下,a*3的积结果保存在一个临时对象中。
int& rd = d 也是类似,在类型转换中会产生临时对象存储中间值。
(注意:引用过程中,可能涉及隐式类型转换、权限的变化)
也就是说,rb和rd引用的都是临时对象。
- 类型转换会产生临时变量(隐式类型转换、强制类型转换)
- 表达式的计算结果会产生临时变量
C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。
int main()
{const int a = 10;int& ra = a; //error// 编译报错:error C2440: “初始化”: ⽆法从“const int”转换为“int &”// 这里的引用是对a访问权限的放⼤// 权限的放大:a是只读的,ra是这块空间的别名,ra的权限是可读可写的//正确写法const int& ra = a; //权限的平移ra++; //error// 编译报错:error C3892: “ra”: 不能给常量赋值int b = 20;const int& rb = b; //权限的缩小// 这里的引用是对b访问权限的缩⼩——权限的平移、缩⼩是允许的rb++; //error// 编译报错:error C3892: “rb”: 不能给常量赋值b++; //不会报错return 0;
}
临时对象 :编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象, C++中把这个未命名对象叫做临时对象。
#include<iostream>
using namespace std;
int main()
{int a = 10;const int& ra = 30; //权限的平移int& rb = a * 3; //errorint& rc = 10; //error// 编译报错: “初始化”: 无法从“int”转换为“int &”//正确写法const int& rb = a*3;const int& rc = 10;double d = 12.34; int& rd = d; //error// 编译报错:“初始化”: 无法从“double”转换为“int &”//正确写法const int& rd = d;return 0;
}
权限的放大、平移、缩小只在引用初始化时表现,在赋值(拷贝)的时候不用考虑这些。
【注意区分】
//权限的放大
const int a = 0;
int& ra = a; //a不能修改,则ra也应该不能修改//不是权限的放大
const int a = 0;
int b = a; //a拷贝给b,b的修改不影响a
再来看一个权限的放大的例子:
int a = 0;
const int* p1 = &a; //p1可以修改,*p1不可以修改,const修饰的是*p1
int* p2 = p1; //errorint* p3 = &a;
const int* p4 = p3; //权限的缩小
这里也是拷贝,把p1的值拷贝给p2,但是*p1不能修改,*p2却可以修改,造成了权限的放大。
p1本身可以修改——p1++;
指针赋值(拷贝)也存在权限的放大、缩小——普通的拷贝不存在权限的放大、缩小。
传参也是一种赋值——实参传给形参。
引用(引用赋值、引用传参)、指针(指针赋值、指针传参)会存在权限的变化
普通赋值、传参不存在。
1.5 引用、传值的效率
1.5.1 引用传参、传值传参的效率
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include <time.h>
struct A
{ int a[10000]; //4万字节
};void TestFunc1(A a){} //拷贝4万字节
void TestFunc2(A& a){} //取别名——>语法上相当于没开空间
、
void TestRefAndValue()
{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();// 分别计算两个函数运行结束后的时间cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; //结果:12cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; //结果:0(小于1ms,向下取整)
}
1.5.2 引用返回、传值返回的效率
#include <time.h>
struct A
{ int a[10000];
};A a;// 值返回
A TestFunc1()
{ return a;
}// 引用返回
A& TestFunc2()
{ return a;
}void TestReturnByRefOrValue()
{// 以值作为函数的返回值类型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();// 计算两个函数运算完成之后的时间cout << "TestFunc1 time:" << end1 - begin1 << endl;cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
1.6 指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
• 语法概念上引用是给一个变量取的别名,不开空间,指针是存储一个变量地址,要开空间。
• 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
• 引用可以直接访问指向对象,指针需要解引用访问指向对象。
• sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
我们来看下引用和指针的汇编代码对比:
语法上不开空间,底层实现是要开空间的。(仿照指针的实现)
转换成汇编之后,就没有引用这个概念了,都是指针。
这就体现了上层(语法上)和底层(物理实现)的不同,引用相当于在顶层对指针进行了封装,达到比指针更方便的使用效果,在使用上可以认为引用不额外开空间。
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求。
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体。
4. 有NULL指针,但没有NULL引用。(并不绝对)
执行结果:卡了几秒,进程才结束,而且结束码为错误码,虽然没报错,但是程序是存在错误的。
正常结束会返回0,在中间过程错误结束会返回一个负的错误码。
第10行只是在语法上有对空指针解引用,在底层上没有对空指针解引用,所以只报警告,不报错。并非看到*ptr就一定是对空指针进行了解引用。
空指针解引用实际不是发生在第10行,而是第11行,第11行输出r,即访问r (*ptr)的时候才会进行解引用。
所以说有时候不能光看语法层,还要集合底层实现来理解。
调试转到反汇编观察:
可以看到上图汇编代码,引用语句没有空指针解引用操作。
这个地方r变成*ptr的本质——即引用的本质: 是开一个(指针)变量去存地址。
即此处相当于用另一个指针,去存一个空指针,不会有问题。
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)。
这里就不能把引用直接看作指针(底层上),而应把引用看作同一块空间的别名(语法上)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
7. 有多级指针,但是没有多级引用。
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
9. 引用比指针使用起来相对更安全。(C++更推荐使用引用)
1.7 必须使用指针的场景
C++的引用不能完全替代指针,以下场景为例:
✅ 1. 指向对象“可为空”时;
引用不能为空,而指针可以。
场景:函数参数或返回值可能“无对象可指”时,必须用指针。
void process(Object* obj) {if (obj) { // 必须检查是否为空obj->doSomething();}
}
✅ 2. 需要“重新指向”不同对象时;
引用一旦绑定就不能改指向,而指针可以随时改变所指对象。
Node* current = head;
while (current) {current = current->next; // 引用无法做到
}
✅ 3. 动态内存管理(new/delete);
动态分配的对象必须用指针保存地址,引用无法管理生命周期。
Object* ptr = new Object(); // 必须用指针接收
delete ptr;
✅ 4. 数组和指针算术(+-访问数组);
引用不支持指针算术(如 ptr + 1
),也无法表示数组的“首地址”。
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr; // 引用无法替代
*(p + 2) = 10;
✅ 5. 实现数据结构(链表、树等)
这些数据结构的核心是“节点通过地址链接”,结点之间的指向必须支持更改,只能用指针。
struct Node {int data;Node* next; // 引用无法表示“下一个节点”
};
✅ 6……
C++的引用的设计者只把引用当作指针的补充,并没想着完全替代指针。
JAVA就没有指针,只有引用,JAVA的引用和C++的引用概念类似,但是有一定的区别。
JAVA的引用可以改变指向。
2. 内联函数-inline
2.1 概念
内联函数:用inline修饰的函数叫做内联函数。
内联函数的优势:编译时C++编译器会在调用内联函数的地方展开内联函数,没有函数调用建立栈帧的开销,内联函数提升了程序运行的效率。
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的 调用。
查看方式:
1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add
(有就是没展开,没有就是展开了)
2. vs编译器debug版本下面默认不展开inline的——debug模式下,编译器默认不会对代码进行优化,这样方便调试。(宏的缺点之一就是不方便调试)
解决办法:
① 改成release版本;(优化非常大,看不清楚过程)
② debug版本想展开,需要设置一下以下两个地方——修改项目属性。
结果如下:
以上是课件的版本。
不同的编译器版本的汇编代码可能不一样(VS2022的2024.3.31版本):
以下这段汇编代码甚至将结果保存到两个临时变量空间后,才取第2个临时变量的结果给c:
(VS2022的2025.8.24版本)
2.2 特性
• inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
• inline对于编译器而言只是一个建议。也就是说,你加了inline,编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。
是否展开的权限在编译器,并没有放给编程人员,加上inline只作为一个给编译器的建议,就是为了避免编程人员把一个很大的函数内联展开,导致可执行程序变大——>安装包变大。
平时下载的应用安装包都是可执行程序的压缩版(压缩包),安装完还要解压。
实例:王者荣耀、三角洲行动、QQ飞车、……安装完都需要解压很长一段时间。
• inline适用于频繁调用的短小函数,对于递归函数、代码相对多一些的函数,加上inline也会被编译器忽略。
例如:Swap函数,在排序算法中,大量数据的排序过程中会频繁调用Swap函数,每次调用都要建立函数栈帧——一定的消耗。
C语言是使用宏(宏常量、宏函数)解决的——C++不喜欢使用宏,因为极易出错。
加了分号,cout << ADD(1,2) << endl;就会报错。
不加里面的括号,传递的a/b是比“+”的优先级更低的表达式时,就会出错,比如“&”、“I”、……
#include<iostream>
using namespace std;// 实现⼀个ADD宏函数的常⻅问题
//#define ADD(int a, int b) return a + b;
//#define ADD(a, b) a + b;
//#define ADD(a, b) (a + b)// 正确的宏实现
#define ADD(a, b) ((a) + (b))
// 为什么不能加分号?
// 为什么要加外面的括号?
// 为什么要加里面的括号?int main()
{int ret = ADD(1, 2);cout << ADD(1, 2) << endl; //回答问题1cout << ADD(1, 2)*5 << endl; //回答问题2int x = 1, y = 2;ADD(x & y, x | y); // -> (x&y+x|y) //回答问题3return 0;
}
写个ADD的宏函数都有一堆坑需要避,Swap的宏函数就更复杂了。
• C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调 试,C++设计了inline目的就是替代C的宏函数。
一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰。
下图为 《C++prime》第五版关于inline的建议:
• 内联的缺点1:函数规模较大时,编译器会忽略内联展开请求,不会展开函数。
• 内联的缺点2:inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
// F.h
#include <iostream>
using namespace std;
inline void f(int i);// F.cpp
#include "F.h"
void f(int i)
{cout << i << endl;
}// main.cpp
#include "F.h"
int main()
{f(10);return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
这个报错一般在有声明无定义时会发生,因为在链接时会拿着函数名去符号表里面找定义的位置。
这里一般就是call f(地址),但是内联函数在本地展开,没有地址,不进符号表。
大型项目一般都要声明和定义分离——原因:
- 方便项目管理(a.h、a.cpp、test.cpp),不然都放在一个文件就会非常大。
- 若函数定义也在.h,那多个包含这个.h的.cpp在编译时就会产生链接错误。
LNK2005:”...”(...)已经在xxx.obj中定义。
因为声明可以有多份,但是定义只能有一份。
若不想声明和定义分离,就可以加一个static修饰:
加static修饰
- static修饰变量可以改变生命周期;
- static修饰函数会改变链接属性——只在当前文件可见,不进符号表。
目的:避免链接错误。
(内联也有类似的效果:没有地址,不进符号表——>目的:提高调用效率)
3. auto关键字
3.1 类型别名思考
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
1. 类型难于拼写
2. 含义不明确导致容易出错
std::map<std::string, std::string>::iterator 是一个类型,但是该类型太长了,特别容易写错。聪明的同学可能已经想到:可以通过typedef给类型取别名,比如:
使用typedef给类型取别名确实可以简化代码,但是typedef有会遇到新的难题:
1. typedef一次只能重命名一个类型,用typedef就需要每次遇到新的长类型都要typedef一遍。
2. typedef有缺陷,有可能写出bug。
- const (char*) p1——第一个const修饰指针p1本身,等价于char* const p1,而const的指针是必须被初始化的。
- const (char*) *p2——第二个const修饰*p2。
在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的 类型。
然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。
- auto可以替代写起来比较长的类型的定义,简化代码。
3.2 auto简介
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的 是一直没有人去使用它,大家可思考下为什么?
//早期C语言的变量定义语句
auto int a = 0; //表示是自动变量——自己销毁
int a = 0; //等价写法——默认是自动变量,归属的函数栈帧结束后就自动销毁
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一 个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
C++11废除了auto的原本用法(本来也不用),给予了auto新的含义、新的用法。
auto:自动类型推导
不给出变量的具体类型,通过右边的初始化,自动推导类型所定义的变量的类型。
typeid可以帮助我们去看标识符的类型。
【注意】
- 使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto 的实际类型。
- 因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
3.3 auto的使用细则
(1)auto与指针和引用结合起来使用
- 用auto声明指针类型时,用auto和auto*没有任何区别。
- 但用auto声明引用类型时则必须加&。
int main()
{int x = 10;auto a = &x; //这两种写法等价——不加*,可以用任意来初始化auto* b = &x; //这两种写法等价——加*必须用指针来初始化auto& c = x;cout << typeid(a).name() << endl;cout << typeid(b).name() << endl;cout << typeid(c).name() << endl;*a = 20;*b = 30;c = 40;return 0;
}
(2)在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{auto a = 1, b = 2; auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
3.4 auto不能推导的场景
(1)auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
// 就不知道函数栈帧开多大的空间,编译的时候转成汇编指令就需要确定函数栈帧开多大
void TestAuto(auto a)
{//……
}
(2)auto不能直接用来声明数组
数组的元素类型不能是auto。
void TestAuto()
{int a[] = {1,2,3};auto b[] = {4,5,6};
}
(3)为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法。
(4)auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有 lambda表达式等进行配合使用。
4. 基于范围的for循环(C++11)
4.1 范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor()
{int array[] = { 1, 2, 3, 4, 5 };for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i){array[i] *= 2;}for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p){cout << *p << endl;}}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。
因此C++11中引入了基于范围的for循环。
自动取数组array中的数据,赋值给e。
自动++,自动判断结束。
void TestFor()
{int array[] = { 1, 2, 3, 4, 5 };//赋值给e的值进行修改,不影响原数组的值,局部变量e只是数组原数据的一份拷贝。for(auto e : array){cout << e << " ";}//如果希望修改数组中的值,可以加一个引用for(auto& e : array){e *= 2;}return 0;
}
范围for的语法
- for循环后的括号由冒号“ :”分为两部分:
- 第一部分是范围内用于迭代的变量;
- 第二部分则表示被迭代的范围——可以是一个数组,不能是一个指针。
至于auto只是用起来方便,不是必须的,接收int数组可以使用int x:
void TestFor()
{int array[] = { 1, 2, 3, 4, 5 };//范围forfor(int x : array){cout << x << " ";}return 0;
}
【注意】与普通循环类似:
- 可以用continue来结束本次循环;
- 也可以用break来跳出整个循环。
范围for规定是从前往后遍历,不支持从后往前遍历——需要借助下标。
C++和JAVA的范围for都是抄的Python的作业。
4.2 范围for的使用条件
1. for循环迭代的范围必须是确定的
- 对于数组而言,就是数组中第一个元素和最后一个元素的范围;
- 对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
【注意】以下代码就有问题,因为for的范围不确定。
——可以是一个数组,不能是一个指针。
void TestFor(int array[])
{for(auto& e : array)cout<< e <<endl;
}
追求时间复杂度最小,所以不支持传参传数组,传数组会退化成传指针(首元素地址)
C、C++都是编译型语言——先编译,生成可执行程序,再执行。——极度追求(运行)效率的语言
Python是解释型语言——边编译边执行,执行过程可能会报错,不追求运行效率。
2. 迭代的对象要实现++和==的操作
关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现在大家了解一下就可以了。
5. 指针空值-nullptr(C++11)
5.1 C++98的指针空值-NULL
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现 不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下 方式对其进行初始化:
void TestPtr()
{int* p1 = NULL;int* p2 = 0;// ……
}
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何 种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
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(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的 初衷相悖。
在C++98中,字面常量NULL既可以是一个整型数字0,也可以是无类型的指针(void*)常量。
但是编译器默认情况下将其看成是一个整形常量0,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
• C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些⿇烦,本想通过f(NULL)调用指针版本的 f(int*)函数,但是由于NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。
f((void*)NULL); 调用会报错。
• C++11中引入nullptr,nullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换 成任意其他类型的指针类型。
使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
可以理解为nullptr是把0强转成了void*,即nullptr = (void*) 0
【注意】
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。