前言:本文章主要用于个人复习,追求简洁,感谢大家的参考、交流和搬运,后续可能会继续修改和完善。
因为是个人复习,会有部分压缩和省略。
目录
一、内联函数
1.基本概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开(不一定会展开,编译器会自己判断是否应该展开。如果展开,编译器则会在编译期间,用函数体替换内联函数的调用)。
2.特点及使用
内联函数省去了函数调用、参数压栈、创建栈帧等开销,内联函数没有压栈的开销,可以提高程序运行效率。我们可以通过观察调用普通函数和内联函数的汇编代码来查看其优势
int Add(int a, int b)
{
return a + b;
}
int main()
{
int ret = Add(1, 2);
return 0;
}
可以从汇编代码看出,内联函数调用时并没有调用函数这个过程的汇编指令。没有被当做函数call了就说明被作为内联了。
定义为内联函数是一种以空间换时间的方法,实际中,代码规模较小并且频繁调用的函数,需要频繁建立栈帧,我们就可以把它们定义成内联,以此来减去其函数压栈的时间开销。
内联函数的特性:
1. inline是一种以空间换时间的做法,省去了调用函数的额外开销。(代码很长或有循环或递归的函数不适宜作为内联函数)
2. inline对编译器只是一个建议,编译器会自动优化,如果函数不符合要求,编译器不会将其设为内联,不用编译器对可实现为内联的要求不同
3. inline的声明和定义不能分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到了。4.直接在类里面定义实现的成员函数,默认是要作为内联函数。
会发生链接错误的根本原因:内联函数没有地址
内联函数的展开、重载函数的确定,都是在编译阶段进行的。
为什么内联的声明和定义要放一起?
因为内联是在预编译时被展开,展开就没有函数地址了,链接就会找不到。内联函数的声明和定义分离的话,内联是加在声明的地方的。内联的声明和定义不能分离,当遇到内联函数的声明时,但是没有看到内联函数的实现,就只能call这个内联函数了,去找这个地址,链接的时候去找这个函数的地址,但是内联函数是不放在符号表里的,因为认为内联函数在调用的地方展开了,所以不需要给它地址了,内联函数的名字不会进符号表。有定义的函数才会进符号表。
能不能所有函数都搞成内联?
例如这里有一个sort函数有100行指令 ,有10个地方调用,总计是多少指令?
有110条。
如果搞成内联函数,有多少条?
100*10,有1000条指令。
指令变多意味着编译出来的可执行程序变大,内存消耗变多,会使人们的体验变差
3.面试题
C语言为了减少小函数的栈帧开销,提供了宏函数,会在预处理阶段展开。
(1)既然C语言已经解决了这种问题,为什么C++还要提供inline函数呢?(宏函数的优缺点、内联函数和宏函数的区别)
宏函数的优点:
a.增强代码复用性
b.合理使用可以提高性能
宏函数的缺点:
a.宏函数不支持调试(在预编译阶段就进行了替换)
b.宏函数代码可读性差,可维护性差,容易出错
c.宏函数没有类型安全的检查
C++的哪些技术替代宏?
a.对于常量的定义:换用const、enum。(在C++里,const定义的变量将直接变成常量,而不是C里的常变量)
b.对于短小函数的定义:换用内联函数。
内联函数和宏函数的区别:
a.宏只做简单替换。而内联函数可以进行参数类型检查(编译时),还有语法判断等功能,且具有返回值。
b.内联函数可以重载。
c.宏定义时要注意书写容易出现歧义,内联函数可读性好,可维护性好。
(2)宏定义和const的区别
处理阶段
- define是在预处理阶段,在编译之前,属于文本插入替换,而const发生在编译、运行过程。
安全性
- define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错
- const常量有数据类型,编译器可以对其进行类型安全检查
内存占用
define只是将宏名称进行替换,在内存中会产生多份相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表。
宏不检查类型;const会检查数据类型。
宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间。
二、auto
1.基本概念
C语言中,使用auto修饰的变量,是具有自动存储器的局部变量。C++中,auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量的类型是由编译器在编译时推导而得的。auto可以根据变量的类型自动确定自己所定义变量的类型。
2.特点及使用
#include <map>
#include <string>
#include <iostream>
using namespace std;
int main()
{
int a = 10;
//int b = a;
auto b = a;//类型声明成auto,可以根据a的类型自动推导b的类型
map<string, string> m;
//map<string, string>::iterator it = m.begin();
//这里可以根据m.begin()自动推导it的类型是map<string, string>::iterator
auto it = m.begin();
return 0;
}
auto有几个地方不能使用:
1.auto不能用来推导函数参数
auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void test(auto a) {}
2.auto不能直接用来声明数组
void test() { int a[] = {1,2,3}; auto b[] = {4,5,6}; //那么可以这样吗?在创建数组时,auto b[0] = a[1]; auto a = 4.1,b = 4;//也不可以 }
3.auto不能用于强制类型转换
注意:使用auto定义变量时必须对其进行初始化,在编译阶段,编译器需要根据初始化表达式来推导auto的数据类型,因此auto并非是一种类型的声明,而是一个类型声明时的占位符,编译器在编译期间会将auto替换为变量实际的类型。
使用auto在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
三、范围for(C++11)
1.基本概念
C++11中引入了基于范围的for循环,for循环后的括号由冒号分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。(e只是数组中值的拷贝,在不使用引用时,是不可以改变e的。e的名字可以改变,不一定非要是e)
2.特点及使用
auto真正的意义在于支持C++11的新语法范围for,新语法遍历,更简单,是数组都可以,容器也可以。范围for可以自动遍历,依次取出元素赋值给e,直到结束(e的名字可以改变)
范围for可以用来遍历数组和容器,范围for的原理和迭代器有关。与普通的循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
如果想通过范围for改变数据,需要加上&
范围for的使用条件
1. for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。注意:以下代码就有问题,因为for的范围不确定,int array[]表示的不是一个数组,它是一个指针,所以不行以下代码这里不可以传,因为这里array是一个指针,C++为了保持效率是不允许传数组的,数组可能很大,代价可能很高,所以这里不能这样。
void test(int arr[]) { for(auto& e : arr) { cout << e << endl; } }
2. 迭代的对象要能实现++和==的操作。
四、nullptr和NULL
在C++中我们使用nullptr,因为C++中NULL其实是被定义成0的
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下是将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)。
NULL实际是一个宏,它的值实际为0或无类型指针(void*)的常量即被定义为((void *)0),某些情况下会出现问题。
在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同
在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr