深入C++学习还必须掌握的一基础知识精讲


课程总目录



一、形参带默认值的函数

  1. 给默认值的时候,从右向左给
int sum(int a, int b = 20);			//合法
int sum(int a = 10, int b = 20);	//合法
int sum(int a = 10, int b);			//不合法
  1. 调用效率的问题
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
}
  1. 定义处可以给形参默认值,声明处也可以给形参默认值
int sum(int a = 10, int b = 20);

int main()
{...}

int sum(int a, int b)
{
	return a + b;
}
  1. 形参给默认值的时候,不管是定义处给,还是声明处给,形参默认值只能出现一次
// 合法,声明可以无数次,定义只能有一次
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的区别是什么

  • Ccosnt修饰的量,可以不用初始化,只是没有机会给他一个合法的值,因为不能作为左值。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与二级指针的三种结合方式:

  1. const int** q;,最近的类型是int,修饰的表达式是**q**q不能被赋值,*qq可以
  2. int* const* q;,最近的类型是int*,修饰的表达式是*q*q不能被赋值,**qq可以
  3. int** const q;,最近的类型是int**,修饰的表达式是qq不能被赋值,**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**,错误

六、引用、左值引用、右值引用

引用和指针的区别:引用是一种更安全的指针

  1. 引用是必须初始化的,指针可以不初始化或者指向nullptr。引用可以保证引用一块内存(所以更安全),但是指针不是必须初始化,所以可能是一个野指针或者nullptr指针(所以不安全)
  2. 保证引用初始化的值必须是可以取地址的,如int &c = 20;这个语句是不行的
  3. 引用只有一级引用,没有多级引用;指针可以有一级指针,也可以有多级指针
/* 引用和指针的区别 */
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*& qq的类型为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
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GeniusAng丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值