目录:
- C++关键字
- 命名空间
- C++输入&输出
- 缺省参数
- 函数重载
- 引用
- 内联函数
- auto关键字(C++11)
- 基于范围的for循环(C++11)
- 指针空值—nullptr(C++11)
1.关键字
C++关键字(C++98)(C++总计63个关键字,C语言32个关键字)
问题:c语言里面有多少关键字?
答:在c98环境下,他有32个
c++中的63个关键字中包含c语言的32个关键字
随口一提:
在c语言和c++中,他们需要的源文件格式不一样
c语言: .c 文件
c++ : .cpp 文件
在vs2013 中,创建源文件时,我们直接输入我们所需要的文件,不加后缀直接enter
他会自动生成.cpp文件格式
但是我们创建.c文件时,我们需要增加后缀,不然会生成.cpp文件
2.命名空间
在C / C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作
用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字
污染,namespace关键字的出现就是针对这种问题的。
在一个作用域中不能出现相同的名字
例如:
在.cpp中下面这段代码就不能正常执行
int main()
{
int a = 10;
int a = 10;
return 0;
}
c语言中总共有两个作用域:函数体中的局部作用域 以及 全局作用域
c语言中解决方式:只能让名字不同
c++:提出命名空间(namespace)
2.1 命名空间定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名
空间的成员。
//总结性举例:
1. 普通的命名空间
//namespace N1 // N1为命名空间的名称
//{
// // 命名空间中的内容,既可以定义变量,也可以定义函数
// int a;
// int Add(int left, int right)
// {
// return left + right;
// }
//}//(和结构体不同的是,结构体这一行}后面有;命名空间没有)
2. 命名空间可以嵌套
//namespace N2
//{
// int a;
// int b;
// int Add(int left, int right)
// {
// return left + right;
// }
//
// namespace N3
// {
// int c;
// int d;
// int Sub(int left, int right)
// {
// return left - right;
// }
// }
//}
namespace N
{
int a = 10;//------------------1
namespace N
{
int a = 10;//----------------2
}
}
//这样定义也可以正常运行,1和2处的a是不同的
3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
//namespace N1
//{
// int Mul(int left, int right)
// {
// return left * right;
// }
//}
//具体化总结:
//注意:以下代码需要在.cpp环境下进行
//(在vs2013中,只需要将你现在的这个代码文件的后缀变为.cpp即可)
//定义格式1
//(1)
//namespace N1
//{
// int a = 10;
// int Add(int left, int right)
// {
// return left + right;
// }
//}//N1这个命名空间里面的a
//
//int a = 20;//全局作用域的变量a
//
//int main()
//{
// int a = 30;//函数里面的局部变量a
// return 0;
//}
//虽然此时我们有三个a但是预编译是没有任何问题的
//因为三个a所处的作用域都不一样,所以说他就没有发生冲突
//(2)
//N1是一个命名空间
//a和Add我们将之称为命名空间里面的成员
//namespace N1
//{
// int a = 10;
// int Add(int left, int right)
// {
// return left + right;
// }
//}//N1这个命名空间里面的a
//
//int a = 20;//全局作用域的变量a
//int a = 10;
//
//int main()
//{
// int a = 30;//函数里面的局部变量a
// return 0;
//}
//此时代码就不能通过,因为全局作用域的变量中有两个a
//(3)
//对里面的a进行访问一下
//N1是一个命名空间
//a和Add我们将之称为命名空间里面的成员
//命名空间:实际就是一个带有名字的作用域
//namespace N1
//{
// int a = 10;
// int Add(int left, int right)
// {
// return left + right;
// }
//}
//
//int a = 20;
//int main()
//{
// int a = 30;
//
// //有多个同名的a处在不同的作用域,在函数中直接访问时,是按照就近原则
// printf("%d\n", a);//30 访问的是函数体中的a
//
// printf("%d\n", ::a);//20 访问的是全局作用域中的a---》::作用域运算符
//
// printf("%d\n", N1::a);//10 访问的是N1命名空间里面的a
//
// //调用命名空间里面的函数
// N1::Add(10, 20);
// return 0;
//}
//定义格式2--命名空间可以嵌套--即:命名空间中还可以继续定义命名空间
//namespace N2
//{
// int a = 40;
// int Sub(int left, int right)
// {
// return left - right;
// }
//
// namespace N3
// {
// int a = 50;
// int Mul(int left, int right)
// {
// return left * right;
// }
// }
//}
//int main()
//{
//
// printf("%d\n", N2::a);//40
// printf("%d\n", N2::N3::a);//50
//
// return 0;
//}
//代码编译是没有任何问题的(.cpp),则可以证明命名空间可以嵌套
//提出问题?
//以下代码是否可以正常编译
//namespace N1//--------------(1)
//{
// int a = 10;
// namespace N2
// {
// int a = 20;
// }
//}
//namespace N3
//{
// namespace N1{};//-------------(2)
//}
可以
(1)和(2)没有任何关系
//
如果想在N3这个命名空间里面用N1这个
//namespace N1
//{
// int a = 10;
// namespace N2
// {
// int a = 20;
// }
//}
//namespace N3
//{
// //namespace N1{};
// int Div(int left, int right)
// {
// printf("%d\n", N1::a);
// return left / right;
// }
//}
//定义格式3
//两个命名空间的名字相同
//在c++中,一个工程中可以出现相同名字的命名空间
//编译器最中将相同名称命名空间合并成一个
//namespace N
//{
// int a = 10;
// int Add(int left, int right)
// {
// return left + right;
// }
//}
//namespace N
//{
// int b = 20;
// int Sub(int left, int right)
// {
// return left - right;
// }
//}
//int main()
//{
// printf("%d\n", N::a);
// printf("%d\n", N::b);
//
// return 0;
//}
//注:相同命名空间里面有相同元素则不能编译过去
2.2命名空间使用方式
(1)直接在成员前增加 N:: (2.1中有详细介绍)
(2)using N::a;相当于是将a当成全局变量
注:当前文件的全局作用域中不能有a,有的话就会冲突
(using也是c++的关键字)
//namespace N
//{
// int a = 10;
// int Add(int left, int right)
// {
// return left + right;
// }
//}
//using N::a;
//int main()
//{
// printf("%d\n", N::a);
// printf("%d\n", a);//均可打印出a=10
//
// return 0;
//}
//(3)using namespace N;相当于将N命名空间中的所有成员当作当前文件的全局变量
// 缺陷:可能会产生冲突
// 如果产生冲突,按照方式1来进行访问
//namespace N
//{
// int a = 10;
// int Add(int left, int right)
// {
// return left + right;
// }
//}
//using namespace N;
//int main()
//{
// printf("%d\n", N::a);
// printf("%d\n", a);//均可打印出a=10
// return 0;
//}
3. C++输入&输出
c++要兼容a语言,c语言中已经有了输入和输出:scanf和printf
问题:为什么c++要重新搞一套输入和输出呢?
//c语言中:
//int main()
//{
// //printf:在输出时,需要记大量的格式控制,格式控制给错时,输出的结果会有问题
// printf("%d\n", 10);
// printf("%f\n", 10.0);
// printf("%c\n", 'a');
//
// //输出的格式控制与输出的数据格式没有对应起来--c语言编译器不会进行释放匹配检测
// printf("%d", 10, 20, 30);
// printf("%d %d %d", 10);
// return 0;
//}
//为了方便,为了简单
注意:在c++旧版本中 输入和输出的头文件时<iostream.h>,比如:vs6.0
新版本中 输入和输出的头文件时
c++为了和c语言进行区分,c++里面的头文件标准库就没有.h后缀
现在新的编译器:+std(标准命名空间)
//#include<iostream>//c++输入输出需要引用的头文件
//using namespace std;
//int main()
//{
// //输出:
// cout << "Hello world!!!" << endl;
// //c代表控制台,他是c++中的标准输出,(这个数据我将来会将其输出到控制台上面)
// //<<输出运算符
// cout << 12.34 << endl;
// cout << "abcdef\n" << endl;
// cout << 888 << " " << "!!!" << endl; //连续输出
// return 0;
//}
//使用方便,不需要记格式控制
//#include<iostream>
//using namespace std;
//int main()
//{
// int a, b;
// float c;
// char d;
//
// cin >> a; //输入a的值
// cout << a << endl; //输出a的值
//
// cin >> a >> b;
// cout << a << " " << b << endl;
//
// cin >> a >> c >> d;
// cout << a << " "<< c << " "<< d << endl;
// return 0;
//}
说明:
- 使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空
间。
注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件
即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文
件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用
+std的方式。 - 使用C++输入输出更方便,不需增加数据格式控制,比如:整形–%d,字符–%c
函数不同的地方
c语言中定义如下的函数
c语言编译器对于函数返回值类型以及参数类型检测不是很严格(下面代码没有问题)
//test1()
//{
// printf("tset1()\n");
//}
//void test2(int a)
//{
// printf("test2(): %d\n", a);
//}
//int main()
//{
// int a = tset1();
// printf("%d\n", a);
//
// test2(10);
// test2(10, 20);
// test2(10, 20, 30);
// return 0;
//}
//在c++环境下
//c++编译器对于函数参数类型以及返回值类型的检测更加严格(下面代码有问题)
//test1()
//{
// printf("tset1()\n");
//}
//void test2(int a)
//{
// printf("test2(): %d\n", a);
//}
//int main()
//{
// int a = tset1();
// printf("%d\n", a);
//
// test2(10);
// test2(10, 20);
// test2(10, 20, 30);
// return 0;
//}
4.缺省参数
4.1 缺省参数概念:缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
//#include <iostream>
//using namespace std;
缺省参数:在声明或定义函数时,给函数的参数待上默认值,在函数调用时,如果传递了实参了则使用实参
如果没有传递实参则使用默认值
//void TestFunc(int a = 10)
//{
// cout << a << endl;
//}
//int main()
//{
// //下面两个调用皆可实现,
// TestFunc(100);//传参100,则用100
// TestFunc(); //未传参,则用10
//
// return 0;
//}
4.2缺省参数的分类
(1)全缺省参数:所有参数都带有默认值
(2)半缺省参数:部分参数带有默认值
全缺省参数:
//#include <iostream>
//using namespace std;
//void TestFunc1(int a = 1, int b = 2, int c = 3)
//{
// cout << a << " " << b << " " << c << endl;
//}
//int main()
//{
// TestFunc1();//abc都是用默认值
// //如果时全缺省参数,当实参没有传递够时,编译器会将缺的参数补充上来
// TestFunc1(10);//相当于TestFunc1(10,2,3)
// TestFunc1(10,20);//相当于TestFunc1(10,20,3)
// //调用TestFunc1--》注意:编译器在调用时,会负责将该函数所需要的参数补全
// //TestFunc1函数参数实际有三个参数,则调用时必须传递三个参数,如果没有传递够,后续参数使用默认值
// TestFunc1(10,20,30);
// return 0;
//}
半缺省参数
注意:
-
半缺省参数必须从右往左依次来给出,不能间隔着给
-
缺省参数不能在函数声明和定义中同时出现
-
缺省值必须是常量或者全局变量
-
C语言不支持(编译器不支持)
-
半缺省参数必须从右往左依次来给出,不能间隔着给
//#include <iostream>
//using namespace std;
void TestFunc1(int a = 1, int b , int c )这样不行
void TestFunc1(int a = 1, int b , int c= 3 )不行
注意:只能从右往左依次给出
//void TestFunc2(int a, int b = 2, int c = 3)
//{
// cout << a << " " << b << " " << c << endl;
//}
//
//int main()
//{
// TestFunc2(10);//TestFun2实际有三个参数,编译器在调用时候会根据默认值将参数补全:
// //相当于TestFunc2(10,2,3);
// TestFunc2(10, 20);//TestFunc2(10, 20,3);
// TestFunc2(10, 20, 30);
// return 0;
//}
- 缺省参数不能在函数声明和定义中同时出现
/#include <iostream>
//using namespace std;
//(1)不行
//函数说明
//void TestFunc(int a);
函数定义
//void TestFunc(int a)
//{
// cout << a << endl;
//}
//int main()
//{
// TestFunc();
// return 0;
//}
//(2)不行
//函数说明
//void TestFunc(int a = 10);
函数定义
//void TestFunc(int a = 10)
//{
// cout << a << endl;
//}
//int main()
//{
// TestFunc();
// return 0;
//}
//(3)可以
//函数说明
//void TestFunc(int a = 10);
函数定义
//void TestFunc(int a)
//{
// cout << a << endl;
//}
//int main()
//{
// TestFunc();
// return 0;
//}
//(4)可以
//函数说明
//void TestFunc(int a);
函数定义
//void TestFunc(int a = 10)
//{
// cout << a << endl;
//}
//int main()
//{
// TestFunc();
// return 0;
//}
为什么?因为两个位置同时给,万一给的默认值不一致,到底使用哪一个
答:缺省参数既可以在函数声明给出,也可以咋函数定义时给出,那么函数声明时给好还是函数定义时给好?
在函数声明时给出好
将函数的声明和定义分离开:
函数声明防止在头文件.h 函数的定义放在源文件中.cpp
5.函数底层命名方式不同—函数重载
5.1 函数重载概念
函数重载 : 是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的
形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题
关键点提炼:多个函数作用域相同,函数名字必须相同,参数列表必须不同
问题:定义一个通用类型的加法函数—任意属于类型都要能处理?
在c语言中不能正常运行
c语言中:函数名字一定不能相同的,如果相同编译时报错:函数重定义
c语言中
//int Add(int left, int right)
//{
// return left + right;
//}
//double Add(double left, double right)
//{
// return left + right;
//}
//char Add(char left, char right)
//{
// return left + right;
//}
//int main()
//{
// return 0;
//}
//不可以运行
//int AddII(int left, int right)
//{
// return left + right;
//}
//double AddDD(double left, double right)
//{
// return left + right;
//}
//char AddCC(char left, char right)
//{
// return left + right;
//}
//int main()
//{
// return 0;
//}
//这种情况下,编译可以
//c++中
//int Add(int left, int right)
//{
// return left + right;
//}
//double Add(double left, double right)
//{
// return left + right;
//}
//char Add(char left, char right)
//{
// return left + right;
//}
//int main()
//{
// Add(10, 20);
// Add(10.0, 20.0);
// Add('1', '2');
// return 0;
//}
c++可以正常运行
名字一样,这样代码的(1)可读性就会比较高(2)不需要花费时间想名字
函数的名字只有一个,编译器会根据所传递实参来确定应该调用哪个函数
同一个名字出现在不同的位置,出现的位置不同,含义就不一样–》一词多义
延申:
1.函数重载必须要在同一个作用域
我们至今了解的作用域包括三个:1.宏2.函数里面3.命名空间
例如:一个函数在宏里面,一个在命名空间里面,这样的两个函数不构成函数重载
参数列表必须要不同:参数个数不同,参数类型不同,类型的次序不同
//void TestFunc()//----------1
//{}
//void TestFunc(int a)//-------------2
//{}
//void TestFunc(double a)//---------------3
//{}
//void TestFunc(int a, double b)//---------------4
//{}
//void TestFunc(double a, int b)//---------------5
//{}
//int main()
//{
// return 0;
//}
//1和2 一个有参数,一个没有参数---参数个数不同
//2和3 一个是int 一个是double ---参数类型不同
//4和5 ---类型的次序不同
2.返回值类型不同,是不会构成函数重载的
问题:为什么返回值类型不同就不能构成函数重载?
void TestFunc()
{}
int TestFunc()
{
return 0;
}
int main()
{
//为什么仅仅返回值类型不同就不能构成函数重载?
//TestFunc();//该位置的函数调用就不知道调用带有返回值的还是没有返回值的TestFunc函数--会缠身二义性
return 0;
}
名字修饰
函数重载调用原理:
int Add(int left, int right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
在编译器编译阶段,会对传递实参的类型进行推演,然后根据推演的结果选择对应类型的函数进行调用
注意:推演完成后,如果有合适的类型重载的函数则调用,否则会发生隐式类型转化–转化之后有合适类型则调用,否则则报错
//int main()
//{
// Add(1, 2); //int ,int --》int类型的加法函数
// Add(1.0, 2.0);//double,double---》double类型的加法函数
//
// Add('1', '2');//char,char--》找Add(char,char)函数,遗憾该函数没有,发现Add(int,int)函数,而char和int之间可以发生隐式类型转化
// //字符和整形之间可以进行隐式类型转化
//
// //Add('hello', 'world');//报错
// //char* char*没有合适的转换供调用,编译时报错
//
// //Add(1, 2.0);
// //double和int之间可以相互转化
// //int double--》Add(int,double),没有该函数
// //此时,会发生隐式类型转化
// //int double--》Add(int,int);
// //int double--》Add(double,double);
// //编译器报错,有两种选择
// //解决方式一:用户自己进行强转
// Add(1,(int)2.0);//通过强制类型转化,这样就可以运行
//
// //解决方式二:用户给出对应的函数重载(重新写一份相关的代码)
// /*double Add(int left, double right)
// {
// return left + right;
// }*/
// return 0;
//}
函数重载:c语言不支持,C++支持
问题:为什么c语言不支持函数重载呢?C++是如何支持函数重载?
为什么c语言不支持函数重载呢?
在c语言中(.c)
//int Add(int left, int right)//c语言编译器最终将该函数名字修改:_Add
//{
// return left + right;
//}
//double Add(double left, double right)//c语言编译器最终将该函数名字修改:_Add
//{
// return left + right;
//}
//int main()
//{
// return 0;
//}
//编译后不成功
原因解释:
(1)
int Add(int left, int right)
{
return left + right;
}
int main()
{
Add(10, 20);
return 0;
}
在声明函数时,函数的名字为Add
函数调用时,内部调用中的过程中的代码截取:
Add(10,20);
0072141E push 14h
00741420 push 0Ah
00721422 call _Add(07210E1h)
但是编译器调用时没有直接使用函数声明时的名字Add,而使用的时_Add
c和C++程序从编辑完到可以运行:
预处理:头文件展开,宏替换。。。
编译:编译器会按照该中语言的语法规则检测代码是否存在与语法问题
汇编:翻译过程—》将回避那指令翻译成对应的二进制格式指令
链接:组装—》将多个目标文件整合成一个文件 + 解决地址问题
可以生成一个可执行程序
int Add(int left, int right);
int main()
{
//push 14h
//push 0Ah
//call 函数的入口地址(_Add)
Add(10, 20);// error LNK2019: 无法解析的外部符号 _Add,该符号在函数 _main 中被引用
Add(1.0, 2.0);//error LNK2019: 无法解析的外部符号 _Add,该符号在函数 _main 中被引用
return 0;
}
当代码运行的时候,如果报错出现 LNK 无法解析的外部符号 ,我们则需要在函数中检查 函数是否定义,全局变量是否定义
c语言编译器最终将该函数(int和double两个函数)名字修改:_Add
自我理解:为什么代码出错
这个代码的运行需要:声明函数,实现函数,主函数
(1)主函数在实现的时候会先找声明函数,声明函数将你的函数名字(Add改为了_Add),因为你的实现函数中没有(_Add),主函数找不到目标实现函数,所以报错
(2)在int 和 double两种类型下,c语言编译器最终将该函数名字修改:Add。无论什么类型都会改为_Add,因此c语言中无法确定应该改为哪一个,所以崩溃(不要让计算机做选择题)
为什么c语言不支持函数重载呢?—》因为c语言编译器对函数名字修时规则非常简单,仅仅知识在函数名之前增加了
为什么c++支持
int Add(int left, int right);//c++语言编译器最终将该函数名字修改:?Add@@YAHHH@Z
double Add(double left, double right);//c++语言编译器最终将该函数名字修改:?Add@@YANNN@Z
int main()
{
//Add(1, 2);// error LNK2019 : 无法解析的外部符号 "int __cdecl Add(int,int)" (? Add@@YAHHH@Z),该符号在函数 _main 中被引用
Add(1.0, 2.0);//error LNK2019: 无法解析的外部符号 "double __cdecl Add(double,double)" (?Add@@YANNN@Z),该符号在函数 _main 中被引用
return 0;
}
(这段代码没有运行函数,所以一定是错误的,当运行时,我们会发现上述错误,有上述错误中我们可以知道声明函数将我们的函数名字修改成Add@@YAHHH@Z)
不同编译器对函数名字修饰的规则都不一样,但是大的方式还是一样的
C++编译器对函数的名字的修饰规则:
vs
? Add@@YAHHH@Z–(猜想)》H:int
? Add@@YANNN@Z–(猜想)》N:double
验证猜想:
double Add(int left, double right);
int main()
{
Add(1, 2.0);//error LNK2019: 无法解析的外部符号 "double __cdecl Add(int,double)" (?Add@@YANHN@Z),该符号在函数 _main 中被引用
return 0;
}
?Add@@YANHN@Z
?开头 之后就是函数的宁子Add 第一个@符号表示函数名字结束 YA之后跟函数的返回值以及参数类型 @Z表示参数列表结束
C++编译器对函数名字修饰时:最终将参数类型信息编译到名字中去了
问题:有些情况下需要c语言工程中调用C++函数
5.3 extern “C”
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,
将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree
两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。
工程是C++工程,但是想要按照C语言的方式编译代码
extern "C" int Add(int left, int right);
int main()
{
Add(1, 2);
return 0;
}
如此以来,我们便可以使Add函数运行的时候按照C语言的形式进行
#if defined(_cplusplus)
extern "c"{
int Add(int left, int right);
int Sub(int left, int right);
int Mul(int left, int right);
int Div(int left, int right);
#endif
//在这里写标准c程序,例如dll到处函数的定义
#ifdef _cplusplus
}
#endif
int main()
{
Add(1, 2);
Sub(1, 2);
Mul(1, 2);
Div(1, 2);
return 0;
}
【面试题】
- 下面两个函数能形成函数重载吗?有问题吗或者什么情况下会出问题?
#include<iostream>
using namespace std;
extern "C" int Add(int left, int right);
int main()
{
Add(1, 2);
return 0;
}
void TestFunc(int a = 10)
{
cout << "void TestFunc(int)" << endl;
}
void TestFunc()
{
cout << "void TestFunc(int)" << endl;
}
答案:
下面两个函数能形成函数重载吗?
(1)两个都在相同的作用域(2)名字一样(3)参数列表中的参数个数不同—》这两个就是函数重载
有问题吗或者什么情况下会出问题?
全缺省的函数与同名的无参函数只能存在一个
2. C语言中为什么不能支持函数重载?
c语言对函数名字只是加了一个_ 没有办法区分
3. C++中函数重载底层是怎么处理的?
c++对此处理的时候会将类型信息存储在里面的
4. C++中能否将一个函数按照C的风格来编译?
能 extern “C”+你需要改变的C++函数
6.引用
问题:c语言传参有几种方式?
答案:传值,传址
//#include <iostream>
//using namespace std;
//void Swap(int left, int right)
//{
// int temp = left;
// left = right;
// right = temp;
//}
//
//void Swap(int* left, int* right)
//{
// int temp = *left;
// *left = *right;
// *right = temp;
//}
//int main()
//{
// int a = 10;
// int b = 20;
// cout << a << " " << b << endl;
//
// //传值方式进行交换
// Swap(a, b);
// cout << a << " " << b << endl;
//
// //传址方式交换
// Swap(&a, &b);
// cout << a << " " << b << endl;
//
// return 0;
//}
之间有什么区别?
1.传值:形参将来接收到的是实参的一份拷贝,在函数中如果对形参进行改变,不会影响外部实参
2.传址:形参将来放置的是实参的地址,通过对形参解引用拿到实参,对形参解引用进行修改,世界修改的就是外部的实参可以通过形参来改变外部的实参
3.传址比传值的效率高:传值需要的是实参本身的一份拷贝,如果实参类型比较大,将来拷贝的副本就比较大
传址需要的是实参地址的一份拷贝,形参只占4个字节(32位系统)
传址不仅效率高,而且节省空间
4.传值比传址更安全—指针必须要判空(值就不需要)
5.传值比传址代码的可读性高(自我理解:传值后的函数看起来比较简单)
有什么优缺点?
传值:
优点:1.代码的可读性高,较安全 2.不想通过形参改变外部的实参时,即使将实参改了也不会影响外部的实参
缺点:1.传参效率低,浪费空间(传递的是实参的副本) 2.如果想要通过形参改变外部实参时,做不到
传址:
优点:1.传参的效率高,节省空间(传递的是实参的地址–32位平台4个字节) 2.可以通过形参改变外部的实参
缺点:1.安全性低,可读性低 2.如果不想通过形参改变外部的实参时可能会产生副作用
问题:能否有种类型,按值的方式进行操作,但是可以达到指针类似的效果
传值,在函数中也是按照值的方式来操作的,但是可以通过形参改变外部的实参
c语言中没有 C++有
C++提出的引用–》就是一个别名
6.1 引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它
引用的变量共用同一块内存空间。
//#include <iostream>
//using namespace std;
类型& 引用变量名(对象名) = 引用实体;
//void TestRef()
//{
// int a = 10;
//
// //ra就是a的别名
// int& ra = a;
//
// ra = 20;
// a = 30;
// //因为ra是a 的别名,因此ra和a在底层就公用一个实体,即共用同一个块内存空间
// cout << &a << " " << &ra << endl;//输出的地址都是相同的 00EFFB30 00EFFB30
//}
//int main()
//{
// TestRef();
// return 0;
//}
发现:引用可以达到指针类似的效果
#include <iostream>
using namespace std;
void SetNULLPtr(int*& p)
{
p = NULL;
}
//虽然二级指针可以通过形参指针达到对外部实参指向指向的修改,但是理解起来比较麻烦
void SetNULLPtr(int** p)
{
//*p 就是实参本身
*p = NULL;
}
//如果需要一级指针,可以直接用引用来代替
//如果需要二级指针,可以直接使用一级指针的引用来代替
int main()
{
int a = 10;
int* pa = &a;
*pa = 20;
int& ra = a;
ra = 30;
//通过SetNULLPtr将pa指针指向空
//需要在SetNULLPtr函数中改变pa指针的指向
SetNULLPtr(pa);
SetNULLPtr(&pa);
return 0;
}
6.2 引用特性
- 引用在定义时必须初始化
#include <iostream>
using namespace std;
int main()
{
int a = 10;
//引用类型的变量在定义是必须要初始化
int& ra = a;
//int& rb;//取的别名是给谁取的
return 0;
}
代码运行不通过,原因error C2530: “rb”: 必须初始化引用
- 一个变量可以有多个引用
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& ra = a;
int& rb = a;
int& rc = a;
cout << &ra << " " << &rb << " " << &rc << " " << &a << endl;
return 0;
}
代码运行输出结果:0115FA00 0115FA00 0115FA00 0115FA00
一个变量可以有多个别名
- 引用一旦引用一个实体,再不能引用其他实体
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& ra = a;
//ra已经是a的别名了
int b = 20;
ra = b;//该条语句不是让 ra去引用b,而是给ra 来进行赋值
cout << &ra << " " << &b << endl;
//&ra = b;//编译失败 &ra:对ra取地址 &让在类型之后表示引用的标记 &放在变量之前表示对该变量取地址
return 0;
}
输出结果:012FFD40 012FFD28
问题:引用变量的生命周期长还是实体的生命周期长?
#include <iostream>
using namespace std;
int main()
{
int b = 0;
if (true)
{
//注意:rb只能在if的范围内使用
int& rb = b;
rb = 30;
}
cout << rb << endl;
return 0;
}
代码编译失败,因为rb只能在if的范围内使用
所以:实体的生命周期长
6.3常引用—》const类型的引用
int main()
{
const int a = 10;//a是不能被修改的
//int& ra = a;//如果普通类型的引用变量ra去引用a,可以修改ra,如果修改ra则就会把a修改掉
return 0;
}
代码编译失败:“初始化”: 无法从“const int”转换为“int &”
#include <iostream>
using namespace std;
int main()
{
//int& rb = 100;//100是一个常量---也不能修改
const int& rb = 100;//正常
return 0;
}
#include <iostream>
using namespace std;
int main()
{
//正常代码
//double d = 13.34;
//double& rd = d;
//rd = 32.34;
//错误代码
//double d = 12.34;
//int& rd = d;
//错误原因:引用的变量必须要和实体的类型相同
//错误改良代码:
double d = 13.24;
const int& rd = d;
d = 23.34;
//代码正常编译
//可以编译成功,按道理来说rd已经是d的别名了,因此修改d,rd也会改变,但实际情况是修改了d,rd没有发生改变
cout << &rd << " " << &d << endl;//结果:012FF978 012FF990
//rd与d的地址实际是不一样的,则rd就不是d的别名,修改d也不会对rd产生任何的影响,rd没有改变则会说的通
//新问题:rd是谁的别名?
//首先,引用的实现语法--引用变量必须与引用实体的类型一致
//rd和d的类型不一致,rd就不能直接引用d,而int和double之间可以发生隐式类型转换
//编译器就创建一个临时的int类型的变量
//对于编译器自己创建的该临时空间
//用户知道该块空间的名字吗?不知道
//用户知道该块空间的地址吗?不知道
//能对该空间的内容进行修改吗?拿不到该块空间,就不能对空间中的内容进行修改--认为该块空间具有常性--即不能被修改
//因此必须使用const int&来引用
return 0;
}
引用的应用场景:
1.概念:直接给某个实体取别名—为了写代码简单
2.作为函数的形参:可以达到指针类似的效果–即可以通过形参改变外部的实参
void Swap(int left, int right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(int* left, int* right)
{
int temp = *left;
*left = *right;
*right = temp;
}
需要一级指针参数的函数直接可以用引用来替代
需要二级指针参数的函数直接可以用一级指针的引用来代替
void SetNULLPtr(int*& p)
{
p = NULL;
}
p实际就是外部实参指针的别名
void SetNULLPtr(int** p)
{
//*p 就是实参本身
*p = NULL;
}
缺陷:难以理解,可读性差
注意:用引用作为函数的参数时,如果不想通过形参改变外部实参,最好加上const来修饰
void TestFunc(const T& a);//T表示一个具体的类型,比如:struct A
C++环境下能用引用就不要用指针
3.用引用作为函数的返回值
注意:不能返回函数栈上的空间(比如:函数中的局部变量)
(栈上面的空间一旦函数返回,栈上面的空间就会被回收,接受的变量就会接收一个非法的空间,就会出问题)
#include <iostream>
using namespace std;
int& Add(int left, int right)
{
int ret = 0;
ret = left + right;
return ret;
}
int main()
{
int& sum = Add(1, 2);
Add(3, 4);
Add(5, 6);
return 0;
}
以
上代码运行的时候,sum在内存中就会不停的变动:3-7-11
注意:返回实体的声明周期只要不随函数的结束而结束即可
以下两种方法皆可
(1)引用全局变量
#include <iostream>
using namespace std;
int g_a = 0;
int& Add(int left, int right)
{
g_a = left + right;
return g_a;
}
int main()
{
int& sum = Add(1, 2);
Add(3, 4);
Add(5, 6);
return 0;
}
(2)引用类型的参数
#include <iostream>
using namespace std;
int& Add(int left, int right, int& ret)
{
ret = left + right;
return ret;
}
int main()
{
int r = 0;
int& sum = Add(1, 2, r);
Add(3, 4);
Add(5, 6);
return 0;
}
延申:例题
(1)
#include <iostream>
using namespace std;
void test(int a)
{}
void test(int& a)
{}
int main()
{
test(10);
return 0;
}
代码运行没有任何问题,但是我们的test(10)调用哪一个函数?
经过运行测试:调用第一个,为什么?
第二个普通类型的引用不能给一个常量取别名
//#include <iostream>
//using namespace std;
一般该种重载不会出现
//void test(int a)
//{}
//void test(int& a)
//{}
//int main()
//{
// test(10);
// int r = 10;
// test(r);
// return 0;
//}
//运行失败,test(r)对需要调用的函数不明确(两个函数均可调用)
(2)int& a 的类型是不是 int&?
int& 名为:int引用类型
验证:
#include <iostream>
using namespace std;
int main()
{
int a = 10;//a在定义时,编译器要给a变量开辟空间
int& ra = a;//ra是引用类型的变量,在定义时,编译器就不会给ra开辟空间
cout << typeid(a).name() << endl;//int
cout << typeid(ra).name() << endl;//int
return 0;
}
typeid(a).name()//验证a的类型通用表达式
int& 经过代码编译输出的是int
所以int&既是int(说的过去) 也是int&(这个更加准确)
4.传值,传址,传引用三种方法进行效率比较
4.1传值和传引用效率比较:
传值和传引用没有可比性,传值每次传递的时候都是给的一份拷贝,也就是先要将这份变量拷贝出来,然后才可以在函数中用
但这个引用直接就是一个别名,我们就不用在这个地方进行啊拷贝,所以说引用传递起来效率更高
验证:
传值 引用 传指针
#include <iostream>
using namespace std;
#include <ctime>//clock
//我们先创建一个结构体,这个结构体非常大里面有10000个元素,因而这个结构体所占的空间就十分巨大
struct A
{
int array[100000];
};
//我们比较下面两个函数的效率
void TestValue(A a)//传值
{}
void TestRef(A& a)//传引用
{}
void TestPtr(A* a)//传指针
{}
//我们用这个比较他们的效率
void TestFunc()
{
A a;
size_t begin1 = clock(); //运行前的时间
//传值,在传参期间需要进行实参的一份拷贝
for (int i = 0; i < 100000; i++)
TestValue(a);//多调用几次,每次调用的时候,都会对TeatValue进行拷贝,每次调用都会对时间所消耗
size_t end1 = clock(); //运行后的时间
//同理
size_t begin2 = clock();
//引用是别名,在传参期间不需要进行拷贝
for (int i = 0; i < 100000; i++)
TestRef(a);
size_t end2 = clock();
size_t begin3 = clock();
//传指针,在传参期间不需要进行对象的拷贝--拷贝的是对象的地址--32位平台下就是4个字节
for (int i = 0; i < 100000; i++)
TestPtr(&a);
size_t end3 = clock();
//打印:
cout << "传值性能: " << end1 - begin1 << endl;
cout << "传引用性能: " << end2 - begin2 << endl;
cout << "传指针性能: " << end3 - begin3 << endl;
}
int main()
{
TestFunc();
return 0;
}
注意:这段代码运行时,如果你的电脑安装杀毒软件,会报错
原因:你的代码运行时,如果需要调用其他程序,则会报错
解决方法:(1)关闭杀毒软件(2)运行后,添加这段代码所在文件夹为“信任”然后再次运行这段代码
运行结果:两个不同电脑
(1)1800左右 3 2
(2)250左右 2 1
引用和指针的数值不确定,有时候引用快,有时候指针快
结论:传引用和传指针的效率差不多
输出的传值性能数值 远比 传引用性能数值大
一般情况下在C++里面传参用的都是引用,尤其是自定义类型,因为自定义型每次要来进行这种拷贝,而自定义类型里
像这种结构体类型,都包含了许多成员,那么这个结构体变量就可能比较大,每一次把这个变量给我拷贝一份,不单纯的耗费时间,也会占用大量空间
结论:传引用效率比传值效率高–注意:在C++中,对于自定义类型的参数一般都是按照引用的方式来传递的
(T& consT&两种方式)看形参需不需要改变外部实参的变量:T&需要 consT&不需要
引用可以达到和指针类似的效果,比如:都可以通过形参来改变外部的实参
问题:指针和引用有什么区别?
(1)在底层实现上:引用就是按照指针的方式来进行实现的
//#include <iostream>
//using namespace std;
//int main()
//{
// int a = 10;
//
// //下面两个代码表现形式有所差异,但是效果没有任何问题
// int* pa = &a;
// *pa = 100;
//
// int& ra = a;
// ra = 100;
//
// return 0;
//}
//看汇编代码
//不需要的我也就不复制了
//int* pa = &a;
// lea eax, [a]//将eax中值放到ra表示的空间中
// mox dword ptr [pa],eax//将eax中值放到pa代表的空间中
//
//*pa = 100;
// mov eax,dword ptr[pa]//取pa空间里面的值,放到eax中
// mov dword ptr [eax],64h//将100(64h)放到pa所指向的空间里面去
//int& ra = a;
// lea eax, [a]
// mox dword ptr [pa],eax
//
// ra = 100;
// mov eax,dword ptr[pa]
// mov dword ptr [eax],64h
lea指令:对某个变量进行取地址
对于ra,实际在地层中:ra引用变量中实际存放的是其引用实体的地址–而地址一般情况下是在指针中存放的–》说明:引用类型的变量在底层实际就是一个指针
结论:引用在底层就是按照指针的方式实现的
(2)函数类型实现
#include <iostream>
using namespace std;
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(int* left, int* right)
{
int temp = *left;
*left = *right;
*right = temp;
}
int main()
{
int a = 10;
int b = 10;
Swap(&a, &b);
Swap(a, b);
return 0;
}
Swap(&a, &b);
lea eax,[b]
push eax
lea ecx,[a]
push ecx
call Swap(0DA124Eh)
Swap(a, b);
lea eax,[b]
push eax
lea ecx,[a]
push ecx
call Swap(0DA11EAh)
函数的形参如果是引用:
在代码层面是按照值的方式来传递的
但是在底层是按照指针的方式来传递的
指针汇编代码:
int temp = *left;
mov eax,dword ptr [left]
mov ecx ,dword ptr [eax]
mov dword ptr [temp],eax
*left = *right;
mov eax,dword ptr [left]
mov ecx,dword ptr [right]
mov edx,dword ptr [right]
mov dword ptr [eax],edx
*right = temp;
mov eax,dword ptr [right]
mov eax,dword ptr [temp]
mov dword ptr [eax],ecx
引用汇编代码:
int temp = left;
mov eax,dword ptr [left]
mov ecx ,dword ptr [eax]
mov dword ptr [temp],eax
left = right;
mov eax,dword ptr [left]
mov ecx,dword ptr [right]
mov edx,dword ptr [right]
mov dword ptr [eax],edx
right = temp;
mov eax,dword ptr [right]
mov eax,dword ptr [temp]
mov dword ptr [eax],ecx
void Swap(int* left,intright)和void Swap(int& left, int& right)最终在底层生成的汇编代码是完全相同的
结论:在底层实现方式上,引用g就是按照指针的方式来实现的,在底层处理中,根本就没有引用一说
指针和引用的区别:
在底层实现上:引用和指针没有任何区别,引用就是按照指针来实现的T&—》(引用和实体结合后就不能指向其他实体了)int const p = &a;
const T&(既不能让他引用其他类型的实体,也不能改变他所引用的实体)—》const int* const
注意:引用变量实际是有空间的,该空间中存放的是其引用实体的地址
你要给一个变量取别名,那我们在代码里面你直接定义一个引用类型的变量,他这个别名就取好了,那么这个别名我们就可以用这个引用变量操作它的实体,但是这个别名是如何操作这个实体的, 他需要这个别名和实体产生关联,如何建立关联,那我们在底层把这个引用按照指针的方式来进行实现,那我们这个引用变量,它实际上也是有空间的,只不过你空间里面放的是它引用实体的地址,有了这个地址之后,我们就可以找到我们操作的实体,就可以按照指针的方式来进行操作
概念:引用是别名,编译器不会给引用类型变量重新开辟内存空间,引用与其引用实体公用公用一块内存空间
为了方便容易理解所以概念这样说
我们需要将概念转化为具体实现:又是将引用按照指针的方式来进行实现的----每一个指针变量都有自己的空间,因此引用实际是有空间的
上述中:概念中说引用没有空间,他和实体公用同一份空间,注意中说引用有空间-------(矛盾了)
概念是概念,这个位置所说的是底层要实现引用的这种技术只能按照指针这种方式来尽心实现,指针就是要保存他指向那块空间所保存的地址,地址不保存起来怎么取访问这块空间(注意概念和实现的区别)
区别:(概念,性质,使用方式)
- 引用在定义时必须初始化,指针没有要求
分步阅读
int main()
{
int* p;//没有对p进行初始化,这个代码没有任何问题,只不过这个指针指向的是随意值
//一般情况下没有合法指向,我们建议指向空,但是我们没有给他初始值,也可以
return 0;
}
int main()
{
int* p;
int& ra;//我们没有对这个ra进行初始化,那么这个引用是谁的别名,语法里面明确规定,必须要进行初始化
return 0;
}
没有初始化,这个代码编译只会失败
int main()
{
int* p;
int a = 10;
int& ra = a;
return 0;
}
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
int main()
{
int* p;
int a = 10;
int& ra = a;
int b = 20;
p = &a;
p = &b;
return 0;
}
即:ra已经等于a了,就不能等于b了
但是指针p可以等于&a后,然后等于&b
下面这种情况指针也不能那样
int main()
{
int* p;
int a = 10;
int& ra = a;
int b = 20;
p = &a;
p = &b;
int* const cp = &a;
//cp = &b;
return 0;
}
-
没有NULL引用,但有NULL指针
指针在没有合法值的时候我们可以让他等于空 -
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
#include <iostream>
using namespace std;
int main()
{
int* p;
int a = 10;
int& ra = a;
int b = 20;
p = &a;
p = &b;
int* const cp = &a;
//cp = &b;
char c = 'a';
char* pc = &c;
char& rc = c;
cout << sizeof(pc)<<endl;//4 在32位平台下,任何指针都占用四个字节
cout << sizeof(rc) << endl;//1 引用类型实际就是其引用实体类型的大小
return 0;
}
问题:上面说,rc中本质是放的是&c
注意区分代码的底层和代码的表层
地层中:引用变量rc中确实放的是其引用实体c的地址
代码层面:rc就是c的别名
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
#include <iostream>
using namespace std;
int main()
{
int* p;
int a = 10;
int& ra = a;
ra++;
int b = 20;
p = &a;
p++;//指针只有指向一段连续的空间,++/--才有意义
p = &b;
return 0;
}
- 有多级指针,但是没有多级引用
#include <iostream>
using namespace std;
int main()
{
有多级指针
int* p1 = NULL;
int** p2 = NULL;
int*** p3 = NULL;
没有多级引用--》其实就没有多级引用这个名词
int b = 10;
int& rb = b;
int&& rrb = b;//没有这种说法
return 0;
}
报错:无法从“int”转换为“int &&”
#include <iostream>
using namespace std;
int main()
{
//没有多级引用--》其实就没有多级引用这个名词---》但是有这种写法
int b = 10;
int& rb = b;
//这也是引用,我们将其称为 右值引用---》C++11这个版本中提出的一个特性
int&& rrb = 10;
return 0;
}
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
#include <iostream>
using namespace std;
int main()
{
int* p;
int a = 10;
int& ra = a;
ra++;//注意:在底层处理上,编译器会对ra进行解引用找到a
return 0;
}
- 引用比指针使用起来相对更安全
例题:
(1)求下列原因
#include <iostream>
using namespace std;
void Swap(int& left, int& right)
{
int temp = left;
left = right;//代码运行到这里,代码崩溃
right = temp;
}
int main()
{
int a = 10;
int b = 10;
int* pb = NULL;
//*pb = 100;;//对空指针解引用,代码在执行期间崩溃了
Swap(a,*pb);//但是该位置的空指针pb解引用时却没有崩溃
return 0;
}
解答过程:
#include <iostream>
using namespace std;
//在底层被翻译成这样:
//void Swap(int* left, int* right)
//{
// int temp = *left;
// *left = *right;
// *right = temp;
//}
//Swap(&a,pb);&a--》int* left(没有问题)
//pb--》int* right(pb指向空)--》所以有问题
void Swap(int& left, int& right)//发现函数的两个参数是引用,在底层编译器将其转化为指针
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a = 10;
int b = 10;
int* pb = NULL;
//*pb = 100;
//在传参时,看起来是按照值的方式传递的
//但编译器在编译代码是,检测到Swap的两个参数是引用类型的
//编译器已经将Swap函数的两个引用类型的参数转化为指针
//因此:Swap(a,*pb)-->编译成Swap(&a,pb);
Swap(a, *pb);
return 0;
}
(2)
#include <iostream>
using namespace std;
int main()
{
const int a = 10;
int* pa = (int*)&a;
*pa = 100;
cout << *pa << endl;//*pa =a
cout << a << endl;
return 0;
}
1.打印什么?
100 10
2.执行完成后a的值是多少?
100
奇怪的是 :a的值已经通过pa指针修改为100但为什么直接输出a的时候,输出的是10,而不是100呢?
const int a = 10;
在输出a的时候,传递的不是a所在空间的值,而是直接传递了一个10
注意:在C++中,被const修饰的已经是常量,而且具有宏替换的属性,但是替换实际在程序编译时
宏:实在预处理阶段发生替换–代码还没有编译—预处理直接替换,不会进行类型替换—安全性不高
const修饰:是在代码编译阶段进行替换—会进行参数类型检测—安全性高
在C++中,建议用const修饰常量代替宏常量—const更完美
宏常量:为了实现简单,会定义一些宏常量
#define MAX_SIZE 100 在程序中所有用到100的位置都可以使用MAX_SIZE来代替,将来发现100不合适,一改全改
#define PI 3.14 可以提高程序的可读性
注意:宏常量是在预处理阶段进行替换的,不会进行参数类型检测,安全性比较低
const和宏的区别
#include <iostream>
using namespace std;
#define pi "3.14159"//万一写错多写了""
void testfunc()
{
int r = 2;
double s = PI*r*r;//直接替换成 double s = "3.14159"*r*r;
cout << s << endl;
}
int main()
{
testfunc();
return 0;
}
const
#include <iostream>
using namespace std;
//const double pi = "3.14";//double类型你给了一个字符串,代码直接就编译不过去(const会进行类型检测)
void Testfunc()
{
int r = 2;
double s = pi * r *r;
cout << s << endl;
}
int main()
{
Testfunc();
return 0;
}
我们能否将const修饰的结果称为常量?–》可以
#include <iostream>
using namespace std;
void Testfunc()
{
const int a = 10;
//要验证被const修饰的a是否为常量,借助定义数组来测试
//因为:定义数组时必须要给出数组的大小,而数组的大小必须时常量
//编译成功,因为a被const修饰,a时一个常量
//注意:在c语言中,被const修饰的变量不能将其称为常量,仍旧时一个变量,只不过该变量不能被修改,即在c语言中,被const修饰的变量是:一个不能被修改的变量,不能将其称为常量
int array[10];//编译成功
int array[a];//编译成功
//编译失败,因为b是一个变量,而定义数组时,需要一个常量来指定数组的大小
int b = 10;
int array2[b];//编译失败
}
int main()
{
Testfunc();
return 0;
}
面试:
说说宏的优缺点;(注意:分点作答,条理清晰)
宏常量
优点:一改全改,降低出错概率,提高代码的可读性
缺点:在预处理阶段进行替换,不会进行类型检测,代码的安全性较低
因此在C++中,提出建议:尽量使用const修饰的常量替换宏常量
宏函数
优点:
1.不是函数,少了函数调用的开销,可以提高程序的运行效率
2.少写一些代码:因为宏函数可以封装多条语句–注意:不是提高了代码的复用率,因为宏函数在预处理阶段展开了
3.可以提高代码的可读性
缺点:
1.宏函数在预处理阶段被替换,不会进行类型检测,代码的可复性比较低
2.不能调试
3.容易出错,再写的时候每个部分都要加括号
4.每个使用宏函数的位置都会被展开,会造成代码的膨胀
5.宏函数可能会有副作用
在C++中,对于宏函数建议尽量使用内联函数进行代替
优点:
1.因为是函数,参数有类型,因此在编译阶段会进行参数类型检测,代码安全性高
2.在Deubug模式下默认不会展开,可以进行调试—也可以通过对编译器设置来验证到底是否展开
3.写的期间不用向宏函数导出加括号,实现简单
4.不会有副作用
5.在编译阶段已经展开了,少了函数调用的开销,可以提高程序的运行效率
缺陷:每个使用内联函数的位置救护都会被展开,会造成代码膨胀
#include <iostream>
using namespace std;
#define MUL(a,b) ((a)*(b))
#define MAX(a,b) (((a)>(b))?(a):(b))
int Mul(int left, int right)
{
return left * right;
}
int main()
{
//Mul是一个函数,在代码执行起家就有函数调用的一些开销:比如开辟栈环境,要进行参数压栈,清理栈空间,要返回,要调用
cout << Mul(2, 3) << endl;//6
//MUL是一个宏函数,不是函数,在预处理阶段已经将MUL宏体展开了
//因为MUL不是函数,则不会有函数调用的一些开销,可以提高程序的运行效率
cout << MUL(2, 3) << endl;//6
cout << 2 * 3 << endl;//6
int x = 10;
int y = 20;
//int z = MAX(x, y);
//cout << z << endl;//20
int z = MAX(x, ++y);
cout << z << endl;//MAX求的较大值是22,不是21,MAX宏产生了副作用
//#define MAX(a,b) (((a)>(b))?(a):(b))这一句代码中b出现两次,因而++y了两次,所以输出22
return 0;
}
- 内联函数
7.1 概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,
内联函数提升程序运行的效率。
#include <iostream>
using namespace std;
//在C++中,被inline关键字修饰的函数称为内敛函数
return left + right;
}
int main()
{
int a = 10;
int //内敛函数的特性:在编译期,编译器会对内联函数进行展开,少了函数调用的开销,可以提高程序的运行效率
inline int Add(int left, int right)
{
b = 20;
int sum = 0;
//检测内敛函数到底有没有展开?
//如果没有展开,sum = Add(a,b);在该条语句中调用Add函数,有call Add
//如果展开,编译器已经用Add函数的函数体替换了函数调用
//Debug模式,调试模式下,Debug模式下,默认是不会展开的---因为Debug为调试模式
//如果展开,就不能调试
//Release模式,发布程序时所采用的模式,Release模式编译器会对代码进行大量优化,让程序在运行时速度更快
//将代码所在文件夹打开会出现Debug和Release两个文件夹,打开Debug会发现(.exe)文件所占空间大小为60kb,而打开Release文件夹,里面的(.exe)所占空间大小为11kb
//因而调试是需要在Debug环境下,发布工程需要在Release环境下
//Debug模式下摸索:
//编译器中:项目--》(这份代码名字)+属性--》配置属性--》C/C++--》常规--》调试信息格式--》程序数据库
// 丨--》优化--》内联函数扩展--》只适用于 _inline(/Ob1)--->应用--》确定
//运行代码,看反汇编 call指令没有了,直接展开Add函数---》内联函数确实会展开(只不过需要对运行环境进行设置)
//Release模式下也可以看,但是编译器认为没用给删除了
sum = Add(a, b);
return 0;
}
Release模式下,main函数相当于成为了以下代码
int main()
{
return 0;
}
因为以下内容就没有输入和输出,下面代码执行或者不执行对代码没有任何影响,因而Release可以将其全部删除
-----------
int a = 10;
int b = 20;
int sum = 0;
检测内敛函数到底有没有展开?
如果没有展开,sum = Add(a,b);在该条语句中调用Add函数,有call Add
如果展开,编译器已经用Add函数的函数体替换了函数调用
Debug模式,调试模式下,Debug模式下,默认是不会展开的—因为Debug为调试模式
如果展开,就不能调试
Release模式,发布程序时所采用的模式,Release模式编译器会对代码进行大量优化,让程序在运行时速度更快
将代码所在文件夹打开会出现Debug和Release两个文件夹,打开Debug会发现(.exe)文件所占空间大小为60kb,而打开Release文件夹,里面的(.exe)所占空间大小为11kb
因而调试是需要在Debug环境下,发布工程需要在Release环境下
sum = Add(a, b);
7.2 特性
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环 / 递归的函数不适宜使用作为内联函数。
- inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环 / 递归等等,编译器优化时会忽略掉内联。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
创建三个文件
头文件
#pragma once
inline int Sub(int left, int right);
源文件
#include"头文件"
如果编译器将Sub当成内联函数来处理
将来编译时,并不会生成真正的Sub函数
inline int Sub(int left, int right)
{
return left - right;
}
主文件
#include"头文件"
int main()
{
因为Sub函数被inline修饰,而且定义和声明分离开
Sub具有文件作用域,只能在其定义文件中使用
在其他文件中不能使用
使用时,会发生链接错误--因为编译器在编译时,发现Sub是内联函数,并没有给Sub函数生成具体函数体
Sub(a,b);
return 0;
}
编译则会报错--减法这个函数找不到
当我们将inline拿掉--编译则没有任何问题
inline int Sub(int left, int right)
{
return left - right;
}
void TestFunc()
{
int a = 10;
int b = 20;
//Sub(a,b);//将会被直接替换成a-b ,则函数//inline int Sub(int left, int right)就不需要地址,如果在其他函数中调用这个函数,就不会有这个函数体
}
面试:宏和内联函数的区别?
面试:在C++中,有什么方式可以替代宏?
- 常量定义 换用const
- 函数定义 换用内联函数
上面均为C++98版本便有了的内容
8. auto关键字(C++11)
8.1 auto简介
在早期C / C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?
c语言中有auto的关键字
auto关键字专门用来修饰函数中定义的变量,表明:该变量为自动存储类型的变量,即该变量会被自动销毁掉
int main()
{
int a = 10;//局部变量,而局部变量在函数结束时本来就会自动销毁
//auto int a = 10;//这样就会显得多余
return 0;
}
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
举例:(1)
#include <iostream>
using namespace std;
int main()
{
auto a = 10;
auto b = "1234";
auto c = 12.34;
注意:auto不是类型
auto此时只是一个占位符
在编译器编译时,编译器推演d的初始化表达式a+10的类型为int,最终用int替换d之前的auto
auto d = a + 10;//编译器编译完成后就会被转化为:int d = a + 10;
cout << typeid(auto).name() << endl;//输出错误
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
输出:
int
char const *
double
int
(2)
#include <iostream>
using namespace std;
int Add(int left, int right)
{
return left + right;
}
int main()
{
auto ret = Add(10, 20);
cout << typeid(ret).name() << endl;//int
//auto e;
//auto在定义变量期间,必须要进行初始化
//因为:编译器在编译期间,要根据变量的初始化表达式来推演该变量的实际类型
//将该实际类型推演出来后替换auto
return 0;
}
8.2 auto的使用细则
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
#include <iostream>
using namespace std;
void TestAuto()
{
auto a = 10;
//使用auto定义指针类型变量时,加不加*都无所谓
auto pa = &a;//auto-->替换为int*
auto *pb = &a;//auto-->替换为int
cout << typeid(pa).name() << endl;//int*
cout << typeid(pb).name() << endl;//int
//注意:auto定义引用类型变量时,加不加*都无所谓
auto& ra = a;//让ra引用a
auto rra = a;//让rra引用a-----//注意:rra并不是a的引用,只是定义了一个rra的变量,该变量使用a来进行初始化
cout << &a << endl;
cout << &ra << endl;
cout << &rra << endl;
//输出结果a和ra地址相同,rra与他们不同
}
int main()
{
TestAuto();
return 0;
}
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
//使用auto在一行定义多个变量时,每个变量的初始化表达式的类型必须一致
auto a = 1, b = 2, c = a + b;
//编译器在推演auto的实际类型时:
//12.34--》auto应为double
//10--》auto应该为int
//就有歧义:编译器现在就不知道auto到底时应该给成int类型呢还是应该给成double类型,即存在二义性
auto d = 12.34, e = 10;//代码编译则会报错
}
int main()
{
TestAuto();
return 0;
}
8.3 auto不能推导的场景
- auto不能作为函数的参数
#include <iostream>
using namespace std;
void TestAutoParam(auto a)
{
a++;
}
int main()
{
return 0;
}
编译失败:参数不能为包含“auto”的类型
注意:auto不能用来声明函数的参数
因为函数的参数在定义时没有初始化,编译器也无法推演形参的实际类型
即使这样也不行:void TestAutoParam(auto a = 10)
在用auto定义形参时,给了缺省值之后为什么也不行?
原因:因为不是所有函数的参数都有默认值
- auto不能直接用来声明数组
#include <iostream>
using namespace std;
void TestAutoArray()
{
int array0[10];
int array1[] = { 1, 2, 3 };//array1是一个整形数组,里面包含三个元素
int array2[10] = { 1, 2, 3 };//array2是一个整形数组,里面包含三个元素,前三个元素为123,其余元素为零
//auto array4[] = { 1, 2, 3 };//经过编译--》数组的元素类型不能是包含“auto”的类型
//注意;auto不能用来定义数组
}
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用
9. 基于范围的for循环(C++11)
9.1 范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
#include <iostream>
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
//打印数组中的元素
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
cout << array[i] << " ";
cout << endl;//1 2 3 4 5 6 7 8 9 0
//对数组中每个元素*2
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
cout << endl;
//打印数组元素
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
cout << *p << " ";
cout << endl;//2 4 6 8 10 12 14 16 18 0
return 0;
}
有一个不太好的点:
在对数组操作时,必须要依靠用户来确定数组的范围
但是:数组定义好了之后,范围就是确定的
问题:既然数组定义好了之后,范围是确定的,那么用户在访问整个数组或者对整个数组进行操作时,能否不用给范围,让编译器自己进行确定
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
范围for循环+配合auto
#include <iostream>
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
//打印数组--e:实际就是array数组中每个元素的一份拷贝
for (auto e : array)
{
e *= 2;//只是为了验证e改变之后,数组中对饮元素是否发生改变--》验证e是否为数组中每个元素的拷贝
cout << e << " ";
}
cout << endl;//2 4 6 8 10 12 14 16 18 0
//对数组中每个元素乘2的操作
//e就是数组中每个元素的别名
for (auto e : array)
e *= 2;
cout << endl;
for (auto e : array)
{
e *= 2;
cout << e << " ";
}
cout << endl;//2 4 6 8 10 12 14 16 18 0
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
因为数组名作为函数的参数时,实际已经退化成一个指针了
#include <iostream>
using namespace std;
void TsetFor(int array[])//void TestFor(int* array)
{
for (auto e : array)//array表示的空间范围不确定
cout << e << " ";
cout << endl;
}
int main()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
return 0;
}
10. 指针空值nullptr(C++11)
发现问题:
#include <iostream>
using namespace std;
void TestFunc(int a)
{
cout << "TestFunc(int)" << endl;
}
void TestFunc(int* pa)
{
cout << "TestFunc(int*)" << endl;
}
int main()
{
int a = 10;
int* pa = &a;
int* pb = NULL;
TestFunc(0);//理论上应该调用整形TestFunc函数
TestFunc(NULL);//NULL是一个空指针,理论上应该调用指针类型的TestFunc函数
//该位置实际调用的是整形类型的TestFunc函数
//#define NULL 0
//TestFunc(NULL)--》在预处理阶段--》TestFunc(0)
//逻辑不符合
return 0;
}
解决方法:用nullptr代替NULL
#include <iostream>
using namespace std;
void TestFunc(int a)
{
cout << "TestFunc(int)" << endl;
}
void TestFunc(int* pa)
{
cout << "TestFunc(int*)" << endl;
}
int main()
{
int a = 10;
int* pa = &a;
int* pb = NULL;
TestFunc(nullptr);//不需要包含任何的头文件
return 0;
}
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。----》32位下4个字节
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr