3.2 使用细节:
使用时必须初始化:
int main() { auto a; return 0; }
原因:
在编译阶段编译器需要根据初始化表达式来推导auto的实际类型,而未初始化变量,则无法进行推导。注意auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
- 不能直接推导出引用类型.
int main() { int a = 3; auto b = a;//如果想b是a的引用,这里并不能达到我们想要的效果. //正确写法 auto& c = a;//此时c就表示是一个引用类型,auto可以推导出是int类型 return 0; }
- 如果是推导指针类型:
此时,auto和auto*没有区别(记住,只有在推导指针类型时没有区别,试着理解一下,因为指针类型可以直接推导出来)
int main() { int a = 3; auto b = &a;//这里auto会自动推导成int*,表示b是一个整形指针int*指向a auto* c = &a;//此处auto会推导称为int与*结合为int* //所以说此处auto和auto*声明变量没区别. return 0; }
- 同一行定义多个变量的情况:
默认将第一个推导出来的类型作为整条语句其他变量的类型.
int main() { auto x = 3, y = 5; //同类型不会报错 auto a = 3, b = 4.5, c = 3; //不同类型会报错,因为会将推导出来的第一个类型作为这条语句所有变量的类型 return 0; }
- 不能用来声明数组:
int main() { auto x[5] = { 1,2,3,4,5 };//报错 return 0; }
- 不能做形参推导:
void test(auto a)//报错 { ...... }
四、内联函数
概念:
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率 .
//默认Debug下,内联不会起作用. inline int add(int a, int b) { return a + b; } int main() { int c = add(2, 3); return 0; }
需要修改默认属性,更加方便我们在debug版本下观察内联函数.
视频教程:
4.1 观察内联函数的实现:
4.2 内联函数的特点
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用.
缺陷:由于是在调用处展开,则代码量将会扩大,也就导致目标文件的增大.
优势:少了调用开辟函数栈帧的开销,提高程序运行效率。
- 你说内联就内联?编译器是不会信任你的,你将编译器搞崩溃了(代码膨胀咋办?
所以inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同**,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、**不是递归、频繁调用的函数采用inline修饰,否则编译器将不会采用=内联方式.
所以不要以外任何情况下内联都是好的,要视情况而定,对于短小的函数,且大量频繁调用的采用内联比较合适.
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到 .
//test.c(主函数区) #include <iostream> using std::cout; using std::cin; using std::endl; #include "add.h" int main() { int a = 2, b = 3; cout << add(a, b) <<endl; } //add.c int add(int a, int b) { return a + b; } //add.h inline int add(int a, int b);
编译器可以编译通过,但是链接不上,因为声明时显示采用内联方式,内联函数的函数名并不会进符号表(因为会展开),而在主函数区中执行add(a, b)时,就找不到函数的地址,因为符号表中找不到.
4.3 相关面试考点:
(1) 宏函数的优缺点?
优点:
1.增强代码的复用性。(在预处理阶段直接进行宏替换)
2.提高性能。(没call指令,不需要为函数开辟栈帧)
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。(标识符不具备指向性)
3.没有类型安全的检查 。(编译器不会报错)
(2) **C++**有哪些技术替代宏?
常量定义 换用const enum
短小函数定义 换用内联函数
五、C++的一颗“语法糖”🍭
5.1 基于范围的for循环
在c++11中,有一种写法是基于范围的for循环,被称为“语法糖”,让我们尝一尝这颗“糖”甜不甜吧?🍭🍭🍭
以前我们打印数组中的每个元素,我们是这样的.
int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; //对数组的每个元素*2 for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) { arr[i] *= 2; } //打印每个元素 for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) { cout << arr[i] << " "; } return 0; }
使用“语法糖”后:
int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; //对数组的每个元素*2 错误写法 //for (auto x:arr) //{ // x *= 2; //} //正确写法 for (auto& x:arr) { x *= 2; } //打印每个元素 for (auto x : arr) { cout << x << " "; } return 0; }
解释:
对于一个有范围的集合而言,程序员来手动再写一遍范围是没有必要的,多余之举,有时候还会写错(例如:忘记下标从0开始)。此事交给任劳任怨的编译器完成比较好,因此C++11标准中引入了基于范围的for循环。
格式:
for循环后的括号由冒号(😃 分为两部分:
第一部分:范围内用于迭代的变量(取名随意,与给变量名起名一样,有意义即可),
第二部分:表示被迭代的范围,数组名表示范围是整个数组.
5.2 “糖虽好,注意粘牙哦!”
for循环迭代的范围必须是确定的:
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void print_for(int array[])//数组传参过来之后就是首元素的地址,而不是整个数组,所以范围不确定 { for (auto& e : array) cout << e << endl; } int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; print_for(arr); return 0; }
六、认识nullptr
在C语言阶段,我们提倡创建一个变量之后,要给定一个初始值,减少一些可能出现的未知错误(例如:野指针,随机值等),对于指针我们经常使用下面这段代码进行初始化.也就是NULL.
#include <stdio.h> int main() { int* p = NULL; return 0; }
那NULL究竟是什么呢?我们右击NULL,在弹出的快捷菜单中,选择“转到定义”命令,可以查看到下面这段解释.
很明显,NULL就是个宏定义,而这里NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
不论采取上面何种定义,在使用空值(NULL)的指针时,都不可避免的会遇到一些麻烦 .
栗子:
void f(int) { cout << "f(int)" << endl; } void f(int*) { cout << "f(int*)" << endl; } int main() { f(0); f(NULL);//这里想调用f(int*)函数 f((int*)NULL); return 0; }
运行结果:
f(int)
f(int)
f(int*)
这里因为NULL被定义为0,所以并没有调用f(int*)函数成功.因为语言必须向下兼容,所以不能直接修改NULL的定义,为了解决这一问题,C++11中引入了新的的nullptr为void*类型.
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的**。**
- 在**C++11中,sizeof(nullptr) **与sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr