文章目录
一、形参带默认值的函数
- 给默认值的时候,从右向左给
int sum(int a, int b = 20); //合法
int sum(int a = 10, int b = 20); //合法
int sum(int a = 10, int b); //不合法
- 调用效率的问题
int sum(int a = 10, int b = 20)
{
return a + b;
}
int main()
{
int a = 10;
int b = 20;
/*
mov eax, dword ptr[ebp-8]
push eax
mov ecx, dword ptr[ebp-4]
push ecx
call sum
*/
int ret1 = sum(a, b);
/*
push 14H
mov ecx, dword ptr[ebp-4]
push ecx
call sum
*/
int ret2 = sum(a);
/*
push 14H
push 0AH
call sum
*/
int ret3 = sum(); //与sum(20, 50);效率一样,立即数也是直接把数push
}
- 定义处可以给形参默认值,声明处也可以给形参默认值
int sum(int a = 10, int b = 20);
int main()
{...}
int sum(int a, int b)
{
return a + b;
}
- 形参给默认值的时候,不管是定义处给,还是声明处给,形参默认值只能出现一次
// 合法,声明可以无数次,定义只能有一次
int sum(int a, int b = 20);
int sum(int a = 10, int b);
int main()
{...}
int sum(int a, int b)
{
return a + b;
}
二、inline内联函数和普通函数的区别
inline int sum(int x, int y)
{
return x + y;
}
int main(){
int a = 10;
int b = 20;
int ret = sum(a, b);
// 此处有标准的函数调用过程
// 参数压栈,函数栈帧的开辟和回退过程。有函数调用的开销
// 而x+y 指令只是mov add mov(函数返回值需要从寄存器mov)
// 可见函数调用开销远远大于指令开销,不值
int ret = sum(a, b);//sum是内联函数,相当于int ret = a + b;
}
inline内联函数:
- 在编译过程中,没有函数的调用开销了,在函数的调用点直接把函数的代码进行展开处理了
- inline函数符号表中不再生成相应的函数符号
sum_int_int
- inline只是建议编译器把这个函数处理成内联函数,但是不是所有的inline都会被编译器处理成内联函数,如递归函数或代码很多的函数
- 在debug版本编译时打断点,反汇编不会出现inline情况,因为debug版本上,inline是不起作用的;inline只有在release版本下才能出现。
g++ -c main.cpp -O2 [不要-g,不然是debug版本]
,objdump -t main.o
就会发现没有sum的符号了
inline内联函数和普通函数的区别:
- 普通函数有调用开销(在汇编层面讲一讲),内联函数直接将代码展开,如果需要短时间大量调用且比较简单的函数,就设置成内联函数
- 内联函数若内联成功,则不会在符号表中生成符号
- 不是加inline就能成功,比如递归函数或代码很多的函数,是否内联成功可以用
objdump -t xxx.o
来查看符号表中有没有函数符号
三、详解函数重载
1、 一组函数,其中函数名相同,但是参数列表的个数或者类型不同,那么这一组函数就称作函数重载。
bool compare(int a, int b){ // compare_int_int
cout << "compare_int_int" << endl;
return a > b;
}
bool compare(double a, double b){ // compare_double_double
cout << "compare_double_double" << endl;
return a > b;
}
bool compare(const char *a, const char *b){ // compare_const char*_const char*
cout << "compare_char*_char*" << endl;
return strcmp(a, b);
}
int main(){
compare(10, 20);
compare(10.0, 20.0);
compare("aaa", "bbb");
}
2、 一组函数要称得上重载,一定先是处于同一个作用域当中。类比变量的作用域,全局变量和局部变量的关系
bool compare(int a, int b){...}
bool compare(double a, double b){...}
bool compare(const char *a, const char *b){...}
int main(){
bool compare(int a, int b); // 函数的声明
// 后两个函数调用会出现精度丢失的警告和数据类型不匹配的报错,这是因为作用域的问题。
compare(10, 20);
compare(10.0, 20.0);
compare("aaa", "bbb");
}
3、 const或者volatile的时候,是怎么影响形参类型的?
先记到这,后面讲到相应内容的时候会理解。
// 1
void func(int a) {}
void func(const int a) {}
//不能算重载,编译会报错。对于编译器来说,const int和int是同一个类型,都是整型
//因此两个函数的形参都是整型,无法通过函数名和形参进行区分,相当于函数重定义
// 2
void func(int *a) {}
void func(const int *a) {} // 这样就可以
// 3
void func(int *a) {}
void func(int *const a) {} // 这样还是不行,又成类型一样了
int main(){
int a = 10;
const int b = 20;
cout << typeid(a).name() << endl; //输出是int
cout << typeid(b).name() << endl; //输出也是int
return 0;
}
4、 一组函数,函数名相同,参数列表也相同,仅仅是返回值不同,叫不叫重载?
答:这不叫重载,因为C++中符号是由函数名和参数列表生成的,和返回值没关系
5、 在编译时期,就已经选择相应的代码段进行编译,因为函数的调用需要实参赋值给形参的入栈
一些问题:
-
C++为什么支持函数重载,C语言不支持函数重载
编译器产生编译代码时,产生符号规则不同。
C++代码产生函数符号的时候,由函数名和参数列表类型组成的,如sum_int_int
C代码产生函数符号的时候,由函数名来决定,如sum
-
函数重载需要注意什么
一组函数要称得上重载,一定是处在同一个作用域当中的。一旦在函数里声明一个函数,其余重载函数就不会再去全局寻找函数。函数重载前提要在同一个作用域里,不在同一个作用域里,谈不上函数重载了。 -
C++和C语言代码之间如何相互调用
C++ 调用 C 代码:无法直接调用。在C++文件中把调用的C函数的声明括在extern "C"{}
里面
C 调用 C++ 代码:无法直接调用。把C++源码括在extern "C"{}
里面/* C++ 调用 C */ // 错误演示,无法直接调用 // c文件 int sum(int a, int b){ //C语言,符号是sum,放在.text段 return a + b; } // cpp文件 int sum(int a, int b); //函数声明,引用,但是是在C++语言中,生成符号为sum_int_int *UND* // 无法解析的外部符号"int __cdecl sum(int,int)" (sum@@TAHHH@Z),该符号在函数 _main中被引用 int main(){ int res = sum(10, 20); cout << "res:" << res << endl; return 0; } // 正确方法 // test.c文件 int sum(int a, int b){ //C语言,符号是sum,放在.text段 return a + b; } // c++.cpp文件 extern "C"{ // 告知C++编译器,函数是在C语言环境下生成的,编译器按照C规则生成符号sum int sum(int a, int b); } int main(){ int res = sum(10, 20); cout << "res:" << res << endl; return 0; } /* C 调用 C++ */ // 正确方法 // cpp文件 extern "C"{ // 告知C++编译器,函数是在C语言环境下生成的,编译器按照C规则生成符号sum int sum(int a, int b){ //C++语言,符号是sum_int_int,放在.text段。如果按照C编译则不是这个符号了 return a + b; } } // c文件 int sum(int a, int b); // cpp文件若不使用extern,这里直接声明的话,报错“无法解析的外部符号 _sum” int main(){ int res = sum(10, 20); printf("ret:%d\n", ret); return 0; }
跨语言通用的办法:
#ifdef __cplusplus extern "C"{ #endif int sum(int a, int b){ return a + b; } #ifdef __cplusplus } #endif
只要是C++编译器,都内置了
__cplusplus
这个宏名
如果C++编译这段话,认识宏__cplusplus
,则编译宏#ifdef...#endif
中的代码,最终全部代码编译为C
如果C编译这段话,不认识宏__cplusplus
,则不编译宏中的代码,最终依旧编译为C
这样做,无论C还是C++都可以编译这段话,都生成C接口的函数 -
怎么理解多态?(其他知识点后面会讲到)
静态(编译时期)的多态:函数重载
动态(运行时期)的多态:。。。 -
什么是函数重载?
函数名相同,参数列表不同;从函数调用点来看,要处于同一作用域,那么这一组函数称为函数重载
四、C和C++中const的区别
1、const
修饰的变量,不能够再作为左值,初始化完成后,值不能被修改
int main(){
const int a = 20;
a = 30; // 不行
}
2、C和C++中const
的区别是什么
-
C:
cosnt
修饰的量,可以不用初始化,只是没有机会给他一个合法的值,因为不能作为左值。const
修饰的量,不叫作常量,叫做常变量。因此不能当作初始化数组时的长度值int arrat[a] = {};
,这是不合法的。int main(){ //const int a; // 编译也通过 const int a = 20; //int arrat[a] = {}; //不合法 int *p = (int*)&a; *p = 30; // a只是不能作为左值被修改,但其内存中的值可以被修改,因此称为常变量 printf("%d %d %d \n", a, *p, *(&a)); // 输出 30 30 30,不管哪种方式,访问的都是a这块内存 }
-
C++:
const
修饰的量必须初始化,否则报错。如const int a = 20;
,被const
修饰的量a
叫做常量。由于在C++中是常量,因此可以作为数组的初始化长度值,即int arrat[a] = {};
是合法的。但是如果初始化的时候赋值不是立即数,而是变量,例如int b =10; const int a = b;
,则此时a就是常变量,此时就变得和C语言代码一样,不能初始化数组长度,因为相当于把所有有a的地方替换为了变量b。int main(){ //const int a; //编译不通过 const int a = 20; int array[a] = {}; //编译的时候相当于直接把a替换成20,即int array[20] = {}; int *p = (int*)&a; *p = 30; // a内存中的内容确实已经改了 printf("%d %d %d \n", a, *p, *(&a)); // 输出20 30 20 // 这里会优化代码*(&a)为a,并且将a进行替换,将a替换成20 // 代码在编译阶段替换为printf("%d %d %d \n", 20, *p, 20); }
重点:在C++中,所有出现
const
常量名字的地方,在编译时都被此常量的初始值替换了,如果初始值是一个立即数,才叫常量,如果初始值是一个其他变量,则叫常变量
五、const和一二级指针的结合应用
在C++里,const
修饰的量叫做常量,和普通变量的区别有:
- 编译方式不同,在编译过程中,所有出现常量名字的地方,都会用常量的初始值替换
- 初始化完成后,不能作为左值
const修饰的量常出现的错误是:
- 不能再作为左值(不能直接修改常量的值)
const int a = 10; a = 20; // 错误
- 不能把常量的地址泄露给一个普通的指针或者普通的引用变量(不能间接修改常量的值)
const int a = 10; int *p = &a; // 错误,int* <= const int* 这里类型转换都错了,同时把常量的地址泄露给了一个普通的指针 *p = 30; // 错误
1. const和一级指针的结合
C++的语言规范:const
修饰的是离它最近的类型。同时,我们重点关注的是const
修饰的是什么表达式
因此有两种const
和指针的结合情况,一种是修饰指针的指向,一种是修饰指针本身。
- 情况一:
const int* p;
最近的类型是int
,修饰的表达式是*p
,即指针指向的内存不能被赋值,即∗p=20;
不合法
但指针本身p
没有被修饰,即const int *p = &a; p = &b;
合法
故这种情况可以任意指向不同的int
类型的内存,但是不能通过指针间接修改指向的内存的值 - 情况二:
int const* p;
最近的类型是int
(不是*
,*
没法定义变量),修饰的表达式是*p
,同情况一
可知,const
放在int
左右都可以,只要保证在*
他俩右边就行 - 情况三:
int* const p;
最近的类型是int*
(这种情况*
不会像情况一二那样给p了,中间隔了const
,所以要给在类型里面),修饰的表达式是p
,即p
本身不能修改,int* const p = &a;
那么p
就永远指向这个内存,不能进行p = &b;
的修改从而指向其他内存。
但*p
没有被const
修饰,*p=20;
是合法的
故这种情况可以通过指针解引用修改指向的内存的值,但不能修改指针p
指向哪块内存 - 情况四:
const int* const p;
,上面两种情况的结合。
第一个const
最近的类型是int
,修饰的表达式是*p
,即情况一
第二个const
最近的类型是int*
,修饰的表达式是p
,即情况三
故这种情况不能修改指针p
指向哪块内存,也不能通过指针间接修改指向的内存的值
回顾最开始的例子
// 错误示范
const int a = 10;
int *p = &a; // 错误,int* <= const int* 这里类型转换都错了,同时把常量的地址泄露给了一个普通的指针
*p = 30; // 错误
// 正确示范
const int a = 10;
const int *p = &a; //这样就可以了
// 虽然把常量地址给出去了,但是这里const修饰*p,不会把a的值改掉
总结:const
和指针的类型转化公式:
int*
← \leftarrow ←const int*
错误const int*
← \leftarrow ←int*
正确int* const
← \leftarrow ←int*
正确const
如果右边没有*
,const
是不参与类型的 (重点!!!)
因此这本质上还是int*
← \leftarrow ←int*
,故正确
int c = 40;
int* p1 = &c;
const int* p2 = &c; // const int* <= int* 正确
int* const p3 = &c; // int* <= int* 正确
int* p4 = p3; // int* <= int* 正确
//上面的初始化都可以,因为c只是普通变量,没有限制
int* q1 = nullptr; // 指针的空值要使用nullptr而不要用NULL,NULL是整值0。
int* const q2 = nullptr; //这里const修饰q2
cout << typeid(q1).name() << endl; //打印int*
cout << typeid(q2).name() << endl; //打印int*
// 即:const如果右边没有指针*的话,const是不参与类型的
2. const和二级(多级)指针的结合
先来看一段代码:
int a = 10;
int* p = &a;
const int **q = &p; // const int** <= int**
如果类比刚刚一级指针const int*
←
\leftarrow
← int*
,你会不会以为到二级指针里也正确?实际上这是错误的!!!
const与二级指针的三种结合方式:
const int** q;
,最近的类型是int
,修饰的表达式是**q
,**q
不能被赋值,*q
和q
可以int* const* q;
,最近的类型是int*
,修饰的表达式是*q
,*q
不能被赋值,**q
和q
可以int** const q;
,最近的类型是int**
,修饰的表达式是q
,q
不能被赋值,**q
和*q
可以
总结:const
和指针的类型转化公式:
int**
← \leftarrow ←const int**
错误const int**
← \leftarrow ←int**
错误!!
const
和多级指针结合,必须两边都有,一边有一边没有都是错的
注意:
int**
← \leftarrow ←int* const*
错误! 因为这里const
修饰的是后面的*
,即可以看做*
← \leftarrow ←const*
本质上还是与一级指针结合的问题,由一级指针的知识可以轻易看出来这是错误的int* const*
← \leftarrow ←int**
正确! 因为这里const
修饰的是后面的*
,即可以看做const*
← \leftarrow ←*
本质上还是与一级指针结合的问题,由一级指针的知识可以轻易看出来这是正确的
即:重点看const修饰的表达式即可,前面的类型可以直接去掉,这样方便我们判断
再来看一开始的代码示例
int a = 10;
int* p = &a;
const int** q = &p; // const int** <= int**
// 修改方法一:
int a = 10;
const int* p = &a;
const int** q = &p;
// 修改方法二:
int a = 10;
int* p = &a;
const int* const* q = &p;
// 第一个const最近的是int,修饰**q
// 第二个const最近的是int*,修饰*q
// 这样**q和*q都不会有被修改的风险了,编译正确
这里如何理解呢?
答:由const int** q = &p;
可知*q
⇔ \Leftrightarrow ⇔p
,也就是说给*q赋值const int b = 20; *q = &b
,会放到p
中,但此时相当于把const int*
放到了int*
中,这是非法的
因此,有两种改法:
- 把
p
的类型改为const int*
,这样p
里就可以存放const int*
类型了- 把
q
的类型改为const int* const*
,即给*q
修饰了const
,不让*q
改值
一些习题:
int a = 10;
const int* p = &a; // const int* <= int*
int* q = p; // int* <= const int*
// 错误
int a = 10;
int* const p = &a; // int* <= int*
int* q = p; // int* <= int*
// 正确
int a = 10;
int* const p = &a; // int* <= int*
int* const q = p; // int* <= int*
// 正确
int a = 10;
int* const p = &a; // int* <= int*
const int* q = p; // const int* <= int*
// 正确
int a = 10;
int* p = &a; // int* <= int*
const int** q = &p; // const int** <= int**,错误
// 错误,刚才二级指针的例子
int a = 10;
int* p = &a; // int* <= int*
int* const* q = &p; // int* const* <= int**,相当于const* <= *,正确
// 正确
int a = 10;
int* p = &a; // int* <= int*
int** const q = &p; // int** <= int**
// 正确
int a = 10;
int* const p = &a; // int* <= int*
int** q = &p; // int** <= int* const*,相当于* <= const*,错误
// 错误
int a = 10;
const int* p = &a; // const int* <= int*
int* const* q = &p; // int* const* <= const int**
// 分开看
// 第一步:const* <= *,正确
// 第二步:int* <= const int*,错误
// 合起来:int* const* <= const int**,错误
六、引用、左值引用、右值引用
引用和指针的区别:引用是一种更安全的指针
- 引用是必须初始化的,指针可以不初始化或者指向
nullptr
。引用可以保证引用一块内存(所以更安全),但是指针不是必须初始化,所以可能是一个野指针或者nullptr
指针(所以不安全) - 保证引用初始化的值必须是可以取地址的,如
int &c = 20;
这个语句是不行的 - 引用只有一级引用,没有多级引用;指针可以有一级指针,也可以有多级指针
/* 引用和指针的区别 */
int main(){
int a = 10;
/* mov dword ptr [a],0Ah */
int *p = &a; // 定义指针可以直接int *p,而不必须初始化,不安全。
/* lea eax, [a] %lea是移地址
mov dword ptr [p],eax */
int &b = a; // &符号前有类型,就是引用,没有类型就是取地址
/* lea eax, [a]
mov dword ptr [b],eax */
// 由上可见:定义指针和定义引用的汇编语言是一模一样的!!!
*p = 20;
/* mov eax,dword ptr [p]
mov dword ptr [eax],14h */
cout << a << " " << *p << " " << b << endl; // 打印 20 20 20
b = 30;
/* mov eax,dword ptr [b]
mov dword ptr [eax],1Eh */
cout << a << " " << *p << " " << b << endl; // 打印 30 30 30
// 说明a,*p,b都是同一块内存
return 0;
}
定义一个指针变量和定义一个引用变量在汇编指令上是没有区别的,在汇编层面,没有引用和指针之分,底层都通过指针的方式进行的
通过引用变量修改所引用内存的值,和通过指针解引用修改指针指向的内存的值,其底层指令也是一样的。
引用的优势:方便
int swap(int *x, int *y){ // 用指针
int temp = *x;
*x = *y;
*y = temp;
}
int swap(int &x, int &y){ // 用引用,方便很多
int temp = x;
x = y;
y = temp;
}
int main(){
int a = 10;
int b = 20;
// swap(&a, &b); // 指针
swap(a, b); // 引用
cout << "a:" << a << " b:" << b << endl;
}
引用数组
int main(){
int array[5] = {};
int *p = array;
// 定义一个引用变量,来引用array数组
int (&q)[5] = array;
// 用指针定义数组指针:int (*q)[5] = &array;
// 把右面的&替换到左边的*,就变成了引用的方式
cout << sizeof(array) << endl; // 20
cout << sizeof(p) << endl; // 4
cout << sizeof(q) << endl; // 20
return 0;
}
左值引用和右值引用:
- 左值:有内存,有名字,值可以修改,如
int a = 10;
,a
就是左值,那么就可以int& b = a;
- 右值:没内存,没名字,如
int& c = 20;
这种是不正确的,其中20
就是右值。
从C++11以后,提供了右值引用
后面面向对象编程的时候会用到,对对象的效率问题提升巨大
右值引用:int&& c = 20;
,专门用来引用右值类型,指令上,可以自动产生临时量,然后直接引用临时量,其中c
可以修改,c = 20;
反汇编之后:
int const& d = 20;
和int&& c = 20;
是等效的,其底层逻辑都是int temp = 20; temp -> d
。但是此时d
是不能修改的,即d = 30;
是不对的,因为它被const
修饰,而c
是可以修改的,c = 30;
一个右值引用变量,本身是一个左值,它有名字、有内存,只能用左值引用引用它,即int& e = c;
是没有问题的,右值变量c
可以被取地址。
而int&& e = c;
是错误的,即不能用一个右值引用变量来引用左值
七、const、一级指针、引用的结合应用
int* p = (int*)0x0018ff44; // 正确
int*& p = (int*)0x0018ff44; // 错误,右边是右值,是立即数,不能用&
int*&& p = (int*)0x0018ff44; // 正确,要用右值引用变量
int* const& p = (int*)0x0018ff44; // 正确
// 右边是不能取地址的,是右值,要用右值引用或者常引用
int a = 10;
int* p = &a;
int*& q = p; // int** q = *p;
// 引用不方便看,还原成指针看!
/*判断对错*/
int a = 10;
int* p = &a;
const int*& q = p; // const int** q = *p; 错误
// const int* <= int*,这样分析是错的,一定要还原成指针才能看出来
两个注意点:
- 引用不参与类型,
const int*& q
中q
的类型为const int*
,可以用typeid(q).name()
打印出来看- 判断对错的时候把引用还原成指针
八、new和delete
new和malloc的区别,delete和free的区别
- malloc和free是C的库函数
- new和delete是C++的运算符
- malloc只开辟内存;new不仅可以做内存开辟,还可以做内存的初始化操作
- malloc开辟内存失败,是通过返回值和
nullptr
作比较;new开辟内存失败,是通过抛出bad_alloc类型的异常来判断的
// malloc, free
int* p = (int*)malloc(sizeof(int));
if (p == nullptr) //内存分配失败是用空指针判断
return -1;
*p = 20; //malloc只开辟内存,需要额外初始化
free(p);
// new, delete
int* p1 = new int(20);
delete p1;
开辟数组内存:
// malloc, free
int* q = (int*)malloc(sizeof(int) * 20);
if (q == nullptr)
return -1;
free(q);
// new, delete
// 开辟数组用中括号
//int *q1 = new int[20]; // 在堆上只负责开辟数组
int* q1 = new int[20](); // 这样会把所有数组元素初始化为0
//int* q1 = new int[20](40); // 错误,只能初始化为0。想要其他数值的初始化要遍历数组,或者用函数fill_n
delete[] q1;
new有多少种??
// 第一种:
int* p1 = new int(20); //开辟失败捕获异常,要用try catch
// 异常示例
try {
int* p1 = new int(20);
cout << "Value at p1: " << *p1 << endl;
delete p1;
} catch (const std::bad_alloc& e) {
std::cout << "Memory allocation failed: " << e.what() << std::endl;
}
// 第二种
int* p2 = new (std::nothrow) int(20);
// nothrow表示在分配失败时不抛出异常,而是返回一个空指针,类似malloc失败的情况
// 异常示例
if (p2 == nullptr) {
cout << "Memory allocation failed." << endl;
} else {
*p2 = 42; // 对分配的内存赋值
cout << "Value at p2: " << *p2 << endl;
delete p2; // 释放内存
}
// 第三种
const int* p3 = new const int(40); //在堆内存中分配了一个值为 40 的常量整数
// 第四种,定位new
int data = 0;
int* p4 = new (&data) int(50);
cout << data << endl; //50