C++入门
文章目录
- C++入门
-
- 框架
- [命名空间 namespace (不常用)](#命名空间 namespace (不常用))
- 命名空间的使用方式(三种)
- [using namespace std;](#using namespace std;)
- \<iostream>
- cout
- endl
- cin
- cout的使用
- 命名冲突
- 缺省参数(省钱的省)
- 缺省参数分类
- 函数重载
- [extern "C"是干啥的(面试题)](#extern “C”是干啥的(面试题))
- 缺省参数会影响函数重载吗?(面试题)
- 引用(别名)
- 引用的使用
- 内联函数inline
- 宏的优缺点?(面试题)
- C++有哪些技术替代宏?
- auto关键字(C++11)
- 基于范围的for循环(C++11)
- 指针空值nullptr(C++11)
框架
cpp
#include <iostream>
using namespace std;
int main()
{
return 0;
}
命名空间 namespace (不常用)
定义命名空间,需要使用到namespace关键字 ,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名
空间的成员。
命名空间的{}的后面不需要加分号,跟结构体不一样
cpp
//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;
}
}
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
这个命名空间,可以放变量进去,也可以放函数进去。就像结构体一样,但是又不太一样,因为不用在main主函数里面再次创建结构体了。这个命名空间拿起来就能用。
同一个工程里面相同名称的命名空间会自动合并。在不同的命名空间里面可以建立相同的变量和相同名称的函数(函数的功能完全不同的那种)
命名空间的使用方式(三种)
因为命名空间里面的成员不可以直接使用
cpp
namespace N
{
int a = 10;
int b = 20;
int Add(int left, int right)
{
return left + right;
}
int Sub(int left, int right)
{
return left - right;
}
}
- 像这种直接使用命名空间里面的成员,就会报错,因为编译器无法识别
cpp
int main()
{
printf("%d\n", a); // 该语句编译出错,无法识别a
return 0;
}
正确方式:
- 加命名空间名称及作用域限定符
这个作用域就是变量的来源,比如上面的例子的命名空间N
而作用域限定符就是 :: ,左边是作用域,右边是作用域里面的成员变量。 作用域名::成员名
cpp
int main()
{
printf("%d\n", N::a);
return 0;
}
- 使用using将命名空间中成员引入
using N::b;
如果经常使用这个变量b , 通过上面这种方式,这个b就是一个全局变量了。就不用再次声明b是来源于哪一个命名空间了。
cpp
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
- 使用using namespace 命名空间名称引入
能把命名空间的一个成员拿出来作为全局变量,所以,也能把这个命名空间全部拿出来当成全局变量。
怎么说呢,感觉,你在命名空间里面定义变量,再解开这个命名空间,还不如直接创建全局变量呢。
cpp
using namespce N;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
Add(10, 20);
return 0;
}
using namespace std;
关于using namespace std;学了命名空间这个概念之后,就会比较好解释了。
首先,std是c++的库
那么这句话就是把c++库里面的所有东西放到这个工程里面了
也就是命名空间的第三种使用方式。
这个方式也是相当于把std这个库全展开了,好处就是不用再声明变量是来自std库了,坏处就是再想命名的时候就会跟库里面的命名冲突了
因为它是std这个命名空间里面的变量或者函数啊什么的。你要是还想再命名一个(同名的有冲突的)函数,也可以自己重新定义一个命名空间。因为命名空间里面的变量不会互相冲突
<iostream>
对于这个头文件 io stream
它是c++的输入流和输出流。就像c语言的<stdio.h>
不过,在c语言中printf,scanf这些函数是<stdio.h>这个库里面的函数。
在c++中,iostream这个库也有自己的函数来实现输入和输出的功能
对于一些,特别特别老的编译器(VC6.0),这个头文件也有这种写法<iostream.h>,因为实在是太老了。而且新版编译器都不支持这种写法,尽量忽略
cout
这个函数是c++中iostream库的输出函数,就跟printf一样。但是这个函数来自std这个命名空间。所以,想要使用这个函数,必须要带iostream头文件。关于std命名空间,有那三种方式如下
- 提前声明整个std命名空间
cpp
#include <iostream>
using namespace std; // 有这句话
int main()
{
cout << "hello World";
return 0;
}
- 给cout加上命名空间的作用域限制符"::"
cpp
#include <iostream>
//using namespace std;
int main()
{
std::cout << "hello World";
// 作用域限制符"::"
return 0;
}
endl
这个函数跟cout的来源一样,iostream库的std命名空间的函数。
作用:换行
cpp
#include <iostream>
using namespace std; // 包整个std命名空间
int main()
{
cout << "hello World"<<endl;
return 0;
}
#include <iostream>
//using namespace std;
int main()
{
std::cout << "hello World"<<std::endl; // 或者自己声明来源哪个命名空间
return 0;
}
之前的换行方式也能继续用,如下:
cpp
#include <iostream>
using namespace std;
int main()
{
cout << "hello World\n";
return 0;
}
cin
相当于scanf,但是也不相同。也省去了自己写输入数据的类型这个步骤,以后就不用指定输入和输出的类型了。
cpp
#include <iostream>
using namespace std;
int main()
{
int i = 1;
double d = 1.11;
cin >> i >> d; // 输入5 5.55
cout << i << " " << d << endl;// 输出5 5.55
return 0;
}
cout的使用
关于cout的使用,和printf有很大的不同。
cout输出的时候,不用区分变量的类型,这个函数可以自动识别变量是什么类型。只要定义过了。自己就会按照定义去输出。
cpp
#include <iostream>
using namespace std;
int main()
{
int i = 1;
double d = 1.11;
cout << i << " " << d << endl; // 先输出i的值,再输出一个空格,再输出d的值,然后换行
// 输出结果是:1 1.11
return 0;
}
对比,下面这种写代码的方式。不难发现,每个std库里面的函数都声明命名空间就很麻烦。所以在日常练习中,不要在乎命名冲突。自己改名。
cpp
#include <iostream>
// using namespace std;
int main()
{
int i = 1;
double d = 1.11;
std::cout << i << " " << d << std::endl;
return 0;
}
命名冲突
由于c++的特性,你在命名的时候,如果不是特别必要,比如不是工程需求啥的,能自己换名称,尽量自己换。
如果真的有必要跟std库起冲突。也可以按照命名空间剩下的两种展开方式
- 只给常用的函数展开,比如cout、cin啥的,用using std::cout;这种方式展开局部对象。
- 不常用的函数就用命名空间作用域限制符来写std::cin;
到此为止算是c++的简单入门了,至少能看懂框架的每行代码是什么意思了
缺省参数(省钱的省)
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
cpp
#include <iostream>
using namespace std;
void Func(int a = 0) // 设计函数参数的时候,可以给一个默认值,这个默认值在调用函数的时候生效。如果调用函数的时候,不给参数,这个默认值就会生效
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值
Func(10); // 传参时,使用指定的实参
return 0;
}
就是设计函数的时候,函数的参数可以给个默认值,就像,int a=0 ,如果是放在以前,就只能是int a。现在可以直接给这个a赋值。当然,如果正常使用这个函数,也就是调用函数的时候该有的参数都有,一开始给a赋的值就无效了。如果少参数,这个a才会生效。就像现在的汽车备胎一样,其他的轮胎不坏,就一直不用备胎,一旦有坏的轮胎,备胎才会有用处。
这个Func();语句调用的时候,就没给参数,所以,a的默认值生效了
而Func(10);这个语句调用的时候给参数了,所以给的参数被函数正常使用了。a的默认值无效。
缺省参数分类
全缺省参数
全缺省的意思就是,所有的参数都有默认值。
cpp
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
半缺省参数
半缺省就是部分参数有默认值。不是真的一半参数有默认值
cpp
void Func(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
- 半缺省参数必须从右往左依次连续来给出,不能间隔着给
就是设计函数参数为缺省参数的时候,不能随便设计,不能说是函数的第一个参数和第三个参数是缺省参数,第二个参数是正常的参数
缺省参数设计的时候,只能是从右往左设计缺省参数
错误示范
cpp
void Func(int a = 0, int b , int c = 20) // 错误方式,其中第一个和第三个是缺省参数,不是从右到左依次连续的,会报错的
{
}
void Func(int a = 0, int b = 10, int c) // 也是错的,必须是从右往左连续
{
}
缺省参数函数的使用
- 对于全缺省参数犹如语句:Func(int a=0, int b=10, int c = 20)
传参的时候可以不给参数调用,也可以只给一个参数,或者只给两个参数,或者全部参数都给。
- **在给部分参数的时候,参数只能从左往右依次赋值。**不能是(,1)或者(,1,)
逗号只是传参的时候,给数据的分隔,而不是告诉函数,你只想给哪个参数传参
cpp
#include <iostream>
using namespace std;
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main()
{
Func(); // 一个参数都不给 // 输出就是10,20,30
Func(1); // 只给一个参数 // 输出就是 1,20,30
Func(1, 2); // 给两个参数 // 输出就是1,2,30
Func(1, 2, 3); // 给三个参数 // 输出就是1,2,3
return 0;
}
- 对于半缺省参数函数
在传参的时候,至少也是必须,给非缺省参数的参数。就像下面这个半缺省参数函数。第一个参数不是缺省参数。那么在调用这个函数的时候,只要要有一个参数,而且这个一个参数从左往右开始赋值,也正好给到第一个参数a。
- 如果不给第一个参数传参就会报错。
剩下的缺省参数可以选择性传参,当然,也只能是从左往右传参。
cpp
#include <iostream>
using namespace std;
void Func(int a, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main()
{
Func(); // error
Func(1); // 只给一个参数 // 输出就是 1,20,30
Func(1, 2); // 给两个参数 // 输出就是1,2,30
Func(1, 2, 3); // 给三个参数 // 输出就是1,2,3
return 0;
}
函数重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题
一个函数有多种定义,和多种调用方式。
如果是在c语言里面只允许一个函数名称使用一次。
函数重载就是可以一定的条件下,使用同名的函数。
cpp
// 这种结构在c语言中会报错
int Add(int left, int right)
{
return left+right;
}
double Add(double left, double right)
{
return left+right;
}
long Add(long left, long right)
{
return left+right;
}
int main()
{
Add(10, 20); // 10是整型,编译器默认是个整型,就会按照int Add(int left, int right)这个语句去办事
Add(10.0, 20.0); // 10.0是浮点型就会去调double Add(double left, double right)
Add(10L, 20L); // 10L的10是long的类型。编译器会找参数是long的函数去调用
return 0;
}
函数重载的要求
- 参数类型不同
cpp
int Add(int left, int right)
{
return left+right;
}
double Add(double left, double right)
{
return left+right;
}
long Add(long left, long right)
{
return left+right;
}
- 参数个数不同 (0个也算个数不同)
cpp
int Add()
{
return 0;
}
int Add(int left)
{
return left;
}
int Add(int left, int right)
{
return left + right;
}
int Add(int left, int right, int mid)
{
return left + right + mid;
}
- 类型顺序不同
cpp
int Add(int i, char ch)
{
return i+ch;
}
int Add(char ch, int i)
{
return i+ch;
}
这三个有一个符合要求,就可以构成重载
对于函数前面的那个返回值,不能作为函数重载的依据
cpp
int Add(int left, int right)
{
return left + right;
}
void Add(int left, int right) // error 不满足函数重载
{
return;
}
int Add(int right, int left) // error 形参的名称对函数重载没有影响,函数重载看的是参数的类型,不是参数名称
{
return left + right;
}
也就是只有返回值不同,不管用
函数重载的使用
cpp
#include <iostream>
using namespace std;
int Add(int i, char ch)
{
cout << i << " " << ch << endl;
return i + ch;
}
int Add(char ch, int i)
{
cout << ch << " " << i << endl;
return i + ch;
}
void Add()
{
}
int main()
{
Add(); // 没有参数,程序先去找有没有这个函数,发现有这个函数void Add(),虽然是空,但是不影响程序
Add('a',5); // 第一个参数是一个字符,第二个参数是整型。于是程序找到了第二个函数int Add(char ch, int i)
Add(10,'b'); // 整型+字符,即第一个函数int Add(int i, char ch)
return 0;
}
/*
输出:
a 5
10 b
*/
函数重载的面试题
- 什么是函数重载?
答:在c++环境里面,函数名相同,但是函数参数不同。就叫函数重载。函数参数的不同可以有三种情况;类型不同 或者 个数不同 或者 顺序不同 ,满足这三种其中一个条件就可以构成函数重载。对函数的返回值没有要求
- C++是如何支持函数重载的?C语言为什么不支持?(难)
答:这个问题和程序的编译链接有关
下面这部分是对程序预处理的复习
对于一个工程来说,至少有三个子文件:list.h list.c test.c
预处理 头文件展开,宏替换,条件编译,去掉注释 生成list.i test.i
编译 检查语法,生成汇编代码 生成 list.s test.s
汇编 把汇编代码转成二进制机器码 list.o test.o
链接 把两个目标文件链接在一起
- 关于链接
- 在链接的前一阶段中的汇编阶段,遗留了许多问题。比如在tset文件中用到一个函数,但是这个函数在另一个.c文件里面实现,所以在汇编阶段会在tset文件标记这个函数的地址在其他文件中。汇编阶段,会有符号表这个概念,符号表里面就是是这个函数的名称和该函数的地址。
- 这个问题在链接阶段实现,也就是把两个文件链接了。
名字修饰(name Mangling)
在编译阶段中,由于编译器对函数名称处理的方式不同,才有了c语言和c++的不同。
在c语言的编译器编译阶段,函数名称会直接保留到汇编语言中,不发生改变
而在c++的编译器的编译下,函数名称会被重新修饰。
cpp
//对同一个函数来说
//比如
int Add(int a,int b)
{
}
void func(int a,double b,int* c)
{
}
// 这两个函数在c语言编译器编译下,函数名还是(Add)和(func)
// 而在c++编译器编译的情况下,函数名发生改变
// 比如(Add)函数名,被改成了(_Z3Addii)
// (_Z3Addii)是什么意思呢?
_Z 是统一的函数名前缀
3 是函数名字符的个数
Add 是原函数名
ii 是函数参数的类型简称,第一个i是函数第一个参数的类型,第二个i是函数第二个参数的类型
// 同理(func)函数名被修饰成(_Z4funcidPi)
_Z 前缀
4 函数名字符数
func函数本来的名字
i 是第一个参数的类型
d 是第二个参数的类型
Pi 是第三个参数的类型
通过这里就理解了C语言没办法支持函数重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。 另外我们也知道了,为什么函数重载要求参数不同!而跟返回值没关系。。。
-
所以函数的参数不同,函数在编译阶段生成的名称就不同
-
因为函数名称不同,所以不同函数有不同的地址
- 再去调用函数的时候,就知道调哪个函数了
-
还是因为c++会对函数名进行修饰,这些修饰也就是区分不同函数的关键
以上演示是在Linux环境下演示,而windows环境下的命名规则更复杂,但是道理是相同的。就不再重复演示了。
extern "C"是干啥的(面试题)
cpp
extern "C" int Add(int left, int right);
int Add(int left,int right)
{
return left+right;
}
int main()
{
Add(1,2);
return 0;
}
官方解释:
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern "C",意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern "C"来解决。
我的理解:
在写工程文件的时候,本来要求你用c++写代码。突然,想让你把其中一个函数让c语言也能识别。然而,你这个工程本来就是c++环境。通过前面的知识,你也知道这两种语言的编译器编译函数名称的不同,想要直接让c语言去用这个函数,是不可能的。但是由于c++编译器兼容c语言。所以,想到了一个方法,也就是在这个函数前面加一个语句entern "C",告诉编译器,这个函数要按照c语言去编译。
既然按照c语言去编译了,那么c语言编译器可以直接食用了。但c++编译器就不一定能食用了。所以设计这个语句的时候也考虑了这个问题。这个语句还完成了一个工作,就是告诉c++编译器,这个函数是c语言版的函数,要按照c语言去食用。所以,这个函数被暂时用c语言编译了。
一个函数对应一个extern "C",想转换几个函数就写几个。一般只会有少量函数给外部程序使用。
总结:
extern "C"语句完成了两个任务
- 告诉c++编译器把这个函数按照c语言去编译 ,因为是按照c语言去编译,所以命名规则也变了,也就不能函数重载啥的了
- 告诉c++编译器,这个函数要按照c语言去食用
缺省参数会影响函数重载吗?(面试题)
下面两个函数能形成函数重载吗?有问题吗或者什么情况下会出问题?
cpp
void Func(int a = 10)
{
cout << "void TestFunc(int)" << endl;
}
void Func(int a)
{
cout << "void TestFunc(int)" << endl;
}
// 根据c++的命名规则,都是看参数类型,这两个函数的函数名相同,参数的类型也相同都是i , 所以无法区分
引用(别名)
引用就是给一个变量取新的名字,而且原来的名字还能接着用。
物理意义上,也就是对同一个空间取多个名字
引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
格式: 类型& 引用变量名(对象名) = 引用实体;
cpp
int main()
{
int a=1;
int& ra=a; // 引用
int& rb=a;
int& rc=rb;
printf("%d %d %d %d\n",a,ra,rb,rc); // 1 1 1 1
printf("%p %p %p %p\n",&a,&ra,&rb,&rc); // 都是同一个地址/空间
rc=2; // 改变rc指向空间的值
printf("%d %d %d %d\n",a,ra,rb,rc); // 2 2 2 2
printf("%p %p %p %p\n",&a,&ra,&rb,&rc); // 都是同一个地址/空间
return 0;
}
注意:引用类型必须和引用实体是同种类型的
引用的特性
- 引用在定义时必须初始化
cpp
int main()
{
int a=1;
int& b; // error 必须初始化
return 0;
}
- 一个变量可以有多个引用
cpp
int main()
{
int a=1;
int& ra=a; // 多个引用
int& rb=a; // 多个引用
int& rc=rb;// 多个引用
return 0;
}
- 引用一旦引用一个实体,再不能引用其他实体
cpp
int main()
{
int a=1;
int& b=a; // 创建a的引用,b // b的类型是int,不是int&
int c=2;
b = c; // 分析此处:是b是a的引用还是c的引用?还是把c的值赋给a或b?
// 经过取地址调试,可以发现,b始终是a的引用。最后a变成了2.
return 0;
}
- 引用取别名的时候,变量的权限可以缩小,但是不能放大
cpp
int main()
{
// 正常
int a=1;
int& b=a;
// 不允许变量权限放大
const int a=1;
int& b=a; // error // a在定义的时候有const修饰,是只读的权限,但是b这个别名在定义的时候没有const修饰,那么b是可读可写的。是权限放大。不被允许
//允许变量权限缩小
int a=1;
int& b=a;
const int& c=a; // 在这里a是可读可写,但c是只读。属于权限缩小。被允许。
// 也就是在使用的时候,c这个变量可以被使用,但是不能被修改。 对于a、b变量可以修改和读取。
return 0;
}
- 引用时可以取不同的类型,但是只允许从变量到别名的权限缩小
cpp
int main()
{
int a=1; // 一个正常的变量
double b=a; // 这里是用 a 的值给 b 赋值 // 这里的 a的值 强制类型转换给了 b // 在类型不同的时候,编译器会进行自动的类型转换,但是又不能改变原来的值a,所以会产生一个临时的double类型的变量,然后再把这个临时的变量的值赋给 b
double& c=a; // error // 在取别名的时候,如果类型不同,会产生一个临时变量,但是这个临时变量不能逆向改变,比如,这里的a的值只能是从int转换为别名的double,可是c这个别名是double的类型,是可读可写的。又因为不能在类型不同的时候通过临时变量逆向改变最开始的变量a。所以编译不通过。
const double& d=a; // yes // 这里对d进行了const限制,虽然是double类型,但是这个类型不会通过临时变量逆向改变a,所以允许转换。// 但是这个别名d的值也就被锁定了
const float& e=a; // yes // e 和 d 同理
return 0;
}
上面的a与b没有直接关系,b只是得到了a的值。b的变化跟a没关系。赋值不涉及权限等问题。
但是c,d,e都是a的别名,他们的改变会影响a。可是类型又不同,还不能逆向改变变量a的值。所以必须用const修饰
在赋值的时候产生的临时文件,是一个常量,也不能说是常量,应该是具有常性。也就是说有常量的性质。
- 关于权限的缩小和放大的规则适用于引用和指针
从现在开始,变量被称为对象。
常引用
就是在引用的时候,加上const进行修饰。加上const 进行修饰之后,引用的类型可以不同。给别名加上const进行限制,相当于创造了一个常量。所以可以换类型。
这个const属于缩小权限,也就是程序对变量修改的权限。当没有const的时候,别名也可以之间修改原变量。加上const限制之后,别名就定死了,不能再改变了。
引用的使用
- 引用 作参数
cpp
// 取别名做参数
void Swap_cpp(int& r1, int& r2) // 这里的引用,只有在传参的时候,才被定义 // r1是a的别名,r2是b的别名
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
// 用指针做参数
void Swap_c(int* r1, int* r2) // r1是a的指针,r2是b的指针
{
int tmp = *r1;
*r1 = *r2;
*r2 = tmp;
}
int main()
{
int a=5,b=10;
Swap_c(&a,&b);
Swap_cpp(a,b);
return 0;
}
- 引用做返回值
在学这块知识的时候,要先了解一个概念。就是函数在传值的时候,一般会产生一个临时变量。这个临时变量是目标变量的临时拷贝,然后才是把临时变量的值交给结果变量。比如,Add(int a,int b),再调用这个函数的时候,不是把值直接给ab变量的,而且ab的临时空间得到了值,再传给ab。
是不是有一点点浪费空间,有一点点臃肿。明明我已经有了一个变量自己的空间,现在还要再申请一个空间再存值取值
取别名这个操作完美解决了这个问题,在取别名这个操作下,想要传值的时候,是直接把这个空间(类似地址的东西)递过去了。
就不需要创建临时变量了。下面的例子也是为了解释这个问题。
cpp
void swap(int r1,int r2) // 这是一个传值的函数,那些值在传过来的时候会先创建临时两个临时副本,然后才把临时空间的值给r1r2两个变量
{
int tmp=r1;
r1=r2;
r2=tmp;
}
void swap(int& r1,int& r2) // 这是一个传引用做参数的函数,那些作为参数的值传过来的时候不用创建临时空间,r1和r2会直接根据传过来的值的空间进行引用
{
int tmp=r1;
r1=r2;
r2=tmp;
}
int Count1() // 这是一个返回值是传值的函数,在这个函数调用结束后会返回一个值,这个值也就是n的值,n是有一个自己的空间的。
{ //在传返回值的时候,这个空间被浪费了,用的是n的临时拷贝空间传出去的。当然传出去的值是放在临时空间的。这个空间具有常量性
static int n=0;
n++;
return n;
}
int& Count2() // 这是一个返回值是传引用的函数,返回值是n的值,n本身有一个空间,因为是传引用出去。所以,直接利用n自己的空间,传值出去了
{
static int n=0; // static不会影响变量的类型,只是会影响变量的生命周期
n++;
return n;
}
int main()
{
int& r1=Conut1(); // error // 因为Conut1是传值返回值,得到的空间是一个临时空间,具有常量性,而r1是int类型,不是常量
int& r2=Conut2(); // COnut2的返回值是n的空间本身,类型是int,而且r2的类型也是int。所以,可以直接接收
const int& r1=Conut1(); // 只有r1被const限制之后具有常量性,才能接收Conut1的返回值
return 0;
}
总结:
- 凡是临时变量都具有常量性
- 传值返回会多一个临时空间,这个临时空间是对值的拷贝
- 而引用返回,直接传回来的空间就是那个值自己的空间
引用的不安全
cpp
int& Add(int a,int b)
{
int c=a+b;
return c;
}
int main()
{
int& ret=Add(1,2);
Add(3,4);
cout << "ret=" << ret << endl; // ret是一个随机值,因为这个ret的空间已经被覆盖了,也可能是原来的值,要看编译器。所以不安全
return 0;
}
在上面这个示例中可以看到,如果Add的返回值c的生命周期只在Add函数内部,但ret是c的别名,这个时候再调用ret,ret虽然是保存c的空间,但是已经是非法访问了。所以引用也是不安全的。
- 很明显,就是c的生命周期太短了。虽然Add算出了结果,这个结果保存在了c的空间里面,ret也是这个空间的别名。但是出了Add函数空间就失效了。
- 所以为了解决这个问题,可以用static,c的生命周期就变成了整个程序有效了。
cpp// 改良版 int& Add(int a,int b) { static int c=a+b; // 这个c只有在函数第一次被调用的时候才定义,也就是说只有Add(1,2)中创建了c,且c的值是3。c已经是一个全局变量了 return c; } int main() { int& ret=Add(1,2); Add(3,4); // 第二次调用函数的时候,c已经被创建好了,所以对于第二次函数调用而言,没有执行任何操作,只是把之前c的值传出来了 cout << "ret=" << ret << endl; // ret = 3 return 0; }
经过static修饰后,c是建立在数据段的常变量。数据段的空间不会被销毁。ret一直是Add(1,2)里面c空间的别名
static修饰过的语句只会执行一次!所以第一次执行过后,c的值就固定了。
cppint& Add(int a,int b) { static int c=a+b; c=a+b; // 除非再给c重新赋值,并且没有static修饰 return c; } int& ret=Add(1,2); // ret=3 Add(3,4); // ret=7
总结:
用引用返回值的时候,要看看这个返回值的生命周期,如果只是在他自己的函数内部有效,就不安全了,因为这块空间可能被使用,或者被覆盖。
如果返回值是一个全局变量,可以考虑用引用返回。
引用的好处
- 少创建一个临时变量------提高效率
- 作输出型参数------提高效率
- 其他作用以后讲
传值返回和传引用返回的差别
cpp
#include <iostream>
#include <time.h>
using namespace std;
struct A
{
int a[10000];
};
A a;
// 值返回
A Func1()
{
return a;
}
// 引用返回
A& Func2()
{
return a;
}
void TestReturnByRefOrValue() // 测试函数返回值是 引用 或 值 的差异
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
Func1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
Func2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "Func1 time:" << end1 - begin1 << endl; // 165 ms
cout << "Func2 time:" << end2 - begin2 << endl; // 1 ms
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
上面的测试中,func1是传值返回,他的返回值是结构体a的临时拷贝
func2的函数返回值是a的引用,也就是a自己本来的空间,没有额外创建新的空间
传值做参数和传引用做参数的差异
cpp#include <iostream> #include <time.h> using namespace std; struct A { int a[10000]; }; void TestFunc1(A a) {} void TestFunc2(A& a) {} void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; // 83 cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; // 0 } int main() { TestRefAndValue(); return 0; }
- 经过上面这两个测试用例发现,无论是传值做参数,还是传值返回,都会浪费一定的效率
- 传引用做参数或返回值都能解决这个问题,也证明了引用可以提高效率
引用作返回值的适用场景
- 返回值是一个全局变量
- 静态变量
引用和指针的区别
在语法上来说,
在创建时,引用,直接就是在原有的空间上取别名。
指针则是新建一个空间,存放原空间的地址。有新的空间产生
但是对于底层来说,不一样。
eax,[a]的意思,是把a的地址给eax。所以,后面就是eax再把地址给b和p.指针和引用在汇编语法上是一样的。
但是在底层逻辑上一样。
- 引用 在定义时必须初始化,指针没有要求
- 引用 在初始化时引用一个实体后,就不能再引用其他实体 ,而指针可以在任何时候指向任何一个同类型
实体 - 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占
4个字节) - 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
内联函数inline
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
内联函数解决的问题:
因为某些函数在程序执行的过程中被频繁调用。哪怕再小的函数,也需要建立函数栈帧,也会有消耗的。
就是从主函数到调用的函数的过程中,会进行压栈的操作。也就是程序会不断的从主函数的栈帧 跳到调用的函数的栈帧。
- c语言中可以使用宏函数
- c++使用内联函数
他们都是在预编译阶段,使用宏替换,在函数中展开的。c语言的做法是有缺点的:宏不能调试、可读性很差
对于c++来说,有了内联函数的概念了,只需要在相应的函数前面,再加上inline语句。这个函数在程序的预编译阶段就会被展开到函数中去。(观察代码的汇编语言中是否有call语句。call语句是使用外函数的意思。当然内联函数就没有call了)
cpp
#include <iostream>
#include <time.h>
using namespace std;
inline void Swap(int& r1, int& r2) // 改成内联函数
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
//void Swap(int& r1, int& r2) // 原函数
//{
// int tmp = r1;
// r1 = r2;
// r2 = tmp;
//}
int main()
{
int a = 1, b = 2;
Swap(a, b);
cout << a << b<<endl;
return 0;
}
很明显,想要把常用函数改成内联函数只需要在函数前面再加个inline就可以了。
这个常用函数还是保持正常的函数结构。------------函数的可读性强
在写函数的时候节省大量的时候,不用多次写重复的语句了。
内联函数的特性
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长/递归的函数不适宜使用作为内联函数。(适用小函数)
- inline对于编译器而言只是一个建议 ,编译器会自动优化 ,如果定义为inline的函数体代码很多/有递归等等,编译器优化时会忽略掉内联。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
如果不用inline展开,函数所占用的空间就是(主函数和内联函数)相加。
用inline展开之后。函数所占用的空间,就是(主函数自己的+内联函数*次数)之和。所以inline的函数要么不能频繁使用,要么自己本身的语句要少
类似递归或者超过20行代码的函数,就不会内联了(编译器自动优化)。
因为内联函数是会展开到主函数中,那么在展开的时候,就不构成函数了,也就没有内联函数的地址。如果此时,函数的声明说,这个函数是内联。程序是找不到函数的。
cpp// F.h #include <iostream> using namespace std; inline void f(int i); // 函数的声明是内联,但是函数的原型不在这个.h文件中 // F.cpp #include "F.h" void f(int i) { cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); // error return 0; }
宏的优缺点?(面试题)
优点:
- 增强代码的复用性。
- 提高性能。
缺点:
- 不方便调试宏。(因为预编译阶段进行了替换)
- 导致代码可读性差,可维护性差,容易误用。(太难设计)
- 没有类型安全的检查 。
C++有哪些技术替代宏?
- 常量定义 换用const
cpp#define N 10 // c语言
cppconst int N = 10; // c++优化版
- 函数定义 换用内联函数
宏函数------------》》》inline函数替换
以上所有的知识都是C++98支持的
auto关键字(C++11)
官方定义:
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
就是说auto这个类型关键字,可以自己推出要定义的变量应该是什么类型。
cpp#include <iostream> using namespace std; int main() { int a = 0; auto b = a; int& c = a; auto& d = a; auto* e = &a; auto f = &a; cout << typeid(b).name() << endl; // typeid(b).name()这个语句可以打印括号里面参数的类型 cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; cout << typeid(e).name() << endl; cout << typeid(f).name() << endl; return 0; } /* 输出: int int int int * __ptr64 // e是指针类型,__ptr64意味着是本机是64位系统 int * __ptr64 // e和f的类型相同.所以,用auto的时候,就不用再自己加"\*"了. */
auto的使用规则
- 使用auto定义变量时必须对其进行初始化
cpp
auto e; // 无法通过编译
-
auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
cpp
int a = 0;
int& b = a;
auto& c = a; // c是对a的引用,c的类型是auto,auto推导出c的类型是int。auto只是一个类型。类型& 变量=变量。才是取别名
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
cpp
void TestAuto()
{
auto a = 1, b = 2; // 允许同一行是一个类型
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同 // 同一行类型不同
}
-
auto不能作为函数的参数
cpp// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 void TestAuto(auto a) {}
因为在编译的时候,这个函数虽然没有被使用,哪怕这个程序也不使用。但是在编译阶段依旧要对这个函数建立栈帧。如果是auto作参数,参数类型未知那么auto所需要的空间(字节)也未知
-
auto不能直接用来声明数组
cpp
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
auto最大的作用
在以后使用容器的时候,可以优化变量的类型。
cpp
#include <iostream>
#include <map> // 未来学习的容器map
using namespace std;
int main()
{
std::map<std::string, std::string> dict;
std::map<std::string, std::string>::iterator it1 = dict.begin(); // 这就是创建了一个变量it1,类型名称很长
auto it2 = dict.begin(); // 使用auto,可以简化那一大段类型名称
return 0;
}
基于范围的for循环(C++11)
cpp#include <iostream> #include <map> using namespace std; int main() { int array[] = { 1, 2, 3, 4, 5 }; // 把array数组乘2倍,再打印出来 // C语言的做法 for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++) { array[i] *= 2; } for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); p++) { cout << *p << endl; } // C++11 范围for的用法 for (auto& e : array) // auto自动类型名,auto&是取别名的意思。auto& e就是 { e *= 2; } for (auto e : array) { cout << e << " "; } return 0; }
上面这个示例中,展示了两种解决问题的方法
一种是c语言的做法,比较简单不再解释
第二种,就是c++11 中"范围for"的使用
for (auto& e : array) // 注意是auto& e,是引用
auto自动类型名,auto&是取别名的意思。auto& e就是一个准备取别名的变量e。:array的意思就是从array这个数组中,从数组的0到结束依次取数值出来。对!就是自动取值,自动结束!
连上前面的auto& e。整体for (auto& e : array),意思就是从array这个数组中取值出来,给这个值取别名 e 。然后再对e进行操作
e *= 2; // e变成原来的二倍,取别名的原空间的值也就变成了二倍。
for (auto e : array) // 注意是auto e,是赋值
意思就是:从这个数组名为array的数组里面,从偏移量为0的地方取值出来,赋值给e。等e完成一次循环之后,再向后移动一个步长,取数组的下一个值出来,交给e。
反正这个"范围for"用起来很方便。
这是c++池里面的一个语法操作,因为用起来实在是方便,就像吃糖一样,也叫语法糖。
目前知道可以这么用,就够了。
范围for的使用条件
-
for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
cpp
void TestFor(int array[]) // 这里的array是一个指针,不是真正的数组名
{
for(auto& e : array) // 会报错
cout<< e <<endl;
}
- 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在大家了解一下就可以了)
指针空值nullptr(C++11)
先上个例子
cpp
#include <iostream>
using namespace std;
void fun(int a)
{
cout << "整型" << endl;
}
void fun(int* a)
{
cout << "整型指针" << endl;
}
int main()
{
fun(0);
fun(NULL);
fun(nullptr);
return 0;
}
/*
输出:
整型
整型
整型指针
*/
很明显,第二个出错了。明明NULL就是指针置空的。但是错了。
原因就是C语言中,NULL是被宏定义出来的。也就是#define NULL 0
NULL就是0,0的类型是int
一个 int 类型的 0,给一个 int* 类型的指针置空???
所以在C++11中,新增一个常量nullptr。
这个新增的常量nullptr的类型是void*
满足了日常写代码对指针的正确初始化的操作。如果还是用NULL,可能会发生意外情况。
nullptr也是宏替换。
-
NULL是int类型的0
-
nullptr是void*类型的0