文章目录
1.命名空间
在C++中给了一个namespace这个关键字,这个关键字弥补了C语言的一个缺陷,我们试想一下,在公司中众多人完成一个工程,那么此时有人设置了 相同的变量,相同的函数名改怎么区分呢?? 于是有了命名空间这个概念,我们可以通过不同的命名空间创建相同的变量名或者函数名!
#include<iostream>
using namespace std;
namespace xiaowang
{
int Add(int x, int y)
{
return x + y;
}
}
namespace xiaoli
{
int Add(int x, int y)
{
return x + y;
}
}
int main()
{
cout<<xiaowang::Add(1, 2)<<endl;
cout<<xiaoli::Add(3, 4)<<endl;
return 0;
}
ps:
一个工程允许有多个相同的命名空间,编译器会将他们合并
一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
1.1 命名空间的使用
1.加命名空间名称及作用域限定符
int main()
{
printf("%d\n", N::a);
return 0;
}
2.使用using将命名空间中成员引入
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
3.使用using namespace 命名空间名称引入
using namespce N;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
Add(10, 20);
return 0;
}
2.缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参
简单来说就是在传参前先将参数给定一个默认值,如果传参的时候没有传这个参数,那么就要那个默认值代替!!
2.1全缺省
int Add(int x = 1, int y = 1, int z = 1)
{
return x + y + z;
}
int main()
{
cout << Add() << endl;
return 0;
}
这里注意一个点就是:
传参的顺序是从左向右的,也就是说如果就传2,4的话,那么x=2,y=4,z=1
2.2 半缺省
int Add(int x , int y , int z = 1)
{
return x + y + z;
}
int main()
{
cout << Add(2,4) << endl;
return 0;
}
int Add(int x , int y=1 , int z = 1)
{
return x + y + z;
}
int main()
{
cout << Add(2,4) << endl;
return 0;
}
看代码就知道了,半缺省就没有将全部的参数都指定一个默认值
注意:
半缺省的缺省参数必须得是从右向左连续指定,为什么这样设定也不难理解,因为传参的顺序是从左向右,如果缺省参数也是从左向右的话,那编译器传参就产生了奇异,如果不连续也是一样!!
3.函数重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题
int Add(int a, int b)
{
return a + b;
}
int Add(int a, int b, int c)
{
return a + b + c;
}
double Add(double a, int b)
{
return a + b;
}
int main()
{
cout << Add(1,2) << endl;
cout << Add(1,2,3) << endl;
cout << Add(1.1,2) << endl;
return 0;
}
如上代码Add就构成了函数重载
问:一下代码是否构成函数重载
int Add(int a, int b)
{
return a + b;
}
int Add(int b, int a)
{
return a + b;
}
不构成,函数名一样,参数数量一样,参数类型顺序一样,故是同一个函数,不构成重载
int Add(int a, int b)
{
return a + b;
}
double Add(int a, int b)
{
return a + b;
}
不构成,函数重载和返回值没有关系,想一下我们在调用函数的时候,不知道返回值是什么,故编译器不可以通过返回值在确定你所指定的函数
double Add(double a, int b)
{
return a + b;
}
double Add(double b, int a)
{
return a + b;
}
不构成,函数名一样,参数数量一样,参数类型顺序一样,故是同一个函数,不构成重载
double Add(double a, int b)
{
return a + b;
}
double Add(int a,double b)
{
return a + b;
}
构成,函数名一样,参数数量一样,但是参数类型顺序不一样,故构成重载
3.1 为什么C++支持函数重载,而C语言不支持?(名字修饰规则)
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接
看下图的流程:
为什么相同的代码(特指重载)在C++可以编译,在C不可以编译,我们在C语言环境下,编译器会报一个链接错误!!
我们知道汇编之后产生.o文件和相对于的符号表,链接过程通过符号表将.o文件合并,
但是C语言中的符号表是根据函数的名称来创建符号表的,所以这就能看出为什么C语言不能进行函数重载,链接的时候根据符号表链接,但是符号表有相同的符号,所以不能链接成功!!!所以C++在生成符号表的过程中做出的改变,C++通过函数名和参数类型来制定符号(不同编译器,制定方案不一样),所以链接的时候不会混淆,故C++可以进行函数重载,而C一 样不能
所以C语言的命名修饰规则就根据函数名,C++根据函数名和参数类型!
根据这个能进一步了解为什么函数重载要求参数不同!而跟返回值没关系
3.2 C语言代码能调用C++创建的库吗?C++的代码能调用C语言创建的库吗?
实验1:C++调用C语言创建的库
下面这个代码是一个用C语言写的栈,我现在将他打包成一个静态库
生成静态库:
我们现在创建一个C++项目来调用这个C语言编译的静态库
但是报错了!
我们看下错误:
报的LNK错误,说明编译通过了但是在链接的时候出现了问题!
这里因为我们没有把刚刚的静态库导入,我们导入一下!
现在我们将静态库导入了,我们再来编译一下:
**wt?**还链接错误?? 大家想一想为什么呢?
命名修饰规则!!!
所以我们采取的方法就是将C++命名修饰规则改成C的命名修饰规则一样的,反之不行哦,因为C语言不兼容C++!!
我们通过extern "C"将Stack.h的所有声明都按照C语言的命名修饰规则来编译
这样就能编译过了!
试验2:C语言调用C++创建的库
创建CPP的静态库
我们发现链接错误了
导入库:
还是发生链接错误
我们这里怎么改呢??
我们CPP调用C的库可以将CPP的代码按照C的命名修饰规则编译,但是C调用CPP的库就不能将C的代码按照CPP的命名修饰规则编译,那怎么办呢???
既然C的代码改不了,那我们就该CPP的库,让CPP代码生成对应的静态库时按照C的命名修饰规则来构建!!
用C的命名修饰规则来创建静态库
结果发现C调用CPP的库时函数未定义:
这是为什么呢???
原因是虽然CPP静态库的静态库按照C的命名修饰规则来构建了,但是我们这一举动是将extern "C"加到Stack.h中了,这就导致C在调用这个Stack.h的时候不认识extern “C”,因为这是CPP中才有的!!
于是我们采用一种特殊的方法
条件编译 宏__cplusplus ,下划线_是两个!!!,这个宏是用来区分C和CPP的一个宏就是CPP认识,C不认识
成功!!!
总结:修改CPP一方的命名修饰规则即可完成C,CPP相互调用!!
4.引用(别名)
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
就和外号一样
格式:类型& 引用变量名(对象名) = 引用实体
int main()
{
int a = 1;
int& b = a;
int& c = b;
return 0;
}
地址都是一样的!!!
所以我们可以通过b或者c改a的值!
4.1引用特征
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
4.2 常引用
int main()
{
const int a = 1;
int& b = a;
return 0;
}
这个代码报错了,因为a是const int 的,但是b是int,而b是a的引用,所以这里是权限放大了,所以报错,权限只能缩小不能放大!!
与之对应的代码:
int main()
{
int a = 1;
const int& b = a;
return 0;
}
看一个奇特的问题!
int main()
{
int i = 1;
double b = i;
double& c = i;
return 0;
}
编译器报错了,我们稍作修改:
int main()
{
int i = 1;
double b = i;
const double& c = i;
return 0;
}
我们加上一个const就不报错了,这是为什么呢??
要想了解这个原因,我们得先了解double b = i是怎么赋值的!
不同类型的变量赋值发生整形提升或者是截断结果会产生一个临时变量,通过临时变量赋值给b,但是临时变量具有常性!
所以double& b = i;引用的不是i,而是临时变量,所以得加上const!!
4.3使用场景
1.做参数(输出型参数,大对象参数提高效率)
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
输出型参数:指这个参数的值未知,要通过函数传出来
大对象参数,提高效率:比如结构体传参
2.做返回值(修改返回对象,大对象返回提高效率)
对比传值返回
int Count()
{
int n = 0;
n++;
// ...
return n;
}
传值返回也就是临时变量做返回值
那么这个代码引用返回会怎么样呢:
int& Count()
{
int n = 0;
n++;
// ...
return n;
}
我们知道n是函数栈帧的一个变量,出了栈帧就销毁了,然后我们这里的引用指向这个栈帧,那么返回不就是错误的吗???
我们做出如下修改:在int n前面加上static,让n放在放在静态区就不会被销毁了
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
总结:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
4.4指针和引用的区别
引用在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
其实在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
看汇编代码:
可以看出指针和引用本质是一样的,只不引用他会自动取地址,自动解引用,所以很方便!
所以引用占用内存大小和指针一样!
区别总结:
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指>向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
附:指针和引用的理解图
abcde就是房子,只不过叫法不一样而已罢了,z是记载地址的本子通过记载的地址可以找到房子,本子占用的大小就是8/4byte
5. 内联函数
5.1概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
如:调用一个短小的函数(1-10行),调用10万次,那么这个会不断的开辟栈帧和销毁栈帧,那就会开辟10万次栈帧,如果使用了内联函数, 那么他就会将函数直接展开,不用去调用栈帧.
C语言最对这个的优化方案----宏函数
C语言通过宏函数来避免创建栈帧,宏函数是一种替换机制,在预处理阶段完成替换,我们来看一段代码:
#define ADD(a,b) a+b //写法一
#define ADD(a,b) (a+b)//写法二
#define ADD(a,b) ((a)+(b))//写法三
int main()
{
int a = 1;
int b = 1;
if (ADD(1, 2));
ADD(1, 2) * 3;
ADD(1 | 2, 2 & 1);
ADD(a, b);
return 0;
}
看我们写一个宏函数可以写出多种格式,但是那种是最好的呢??
写法三是最终版本!! 通过宏函数替换避免创建栈帧!!!
所以宏的优缺点:
优点:宏函数提高效率,减少栈帧创建
缺点:可读性差,没有类型检查,不方便调试
内联函数优化宏函数
在概念那段我讲过,内联函数可能会不创建栈帧,我们直接看汇编代码!
先改一下设置:
不然的话内联函数优化的我们看汇编都看不出来
先看一下我们不用内联函数:
void func()
{
printf("func");
}
int main()
{
func();
return 0;
}
通过汇编可以看出来call了func了,就是调用func函数了,下面是跳转到func函数的汇编:
我们看一下内联函数优化的版本:
inline void ADD(int i, int j)
{
cout << i + j;
}
int main()
{
ADD(1, 2);
return 0;
}
看是不是直接在main函数中展开了!
这就是内联函数.
我们看组对比:
inline void func1()
{
printf("func1");
}
inline void func2()
{
printf("func2");
printf("func2");
printf("func2");
printf("func2");
printf("func2");
printf("func2");
printf("func2");
printf("func2");
printf("func2");
printf("func2");
printf("func2");
}
int main()
{
func1();
func2();
return 0;
}
func1直接展开了,而func2是调用的没有展开!
5.2特性
假设有个函数100行指令,调用1万次
那么不展开是一共有1w(调用的call指令)+100条指令
展开是1w*100条指令,所以展开可执行程序会变大
1.inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
2.inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
面试题
宏的优缺点?
优点:
- 增强代码的复用性。
- 提高性能。
缺点:
- 不方便调试宏。(因为预编译阶段进行了替换)
- 导致代码可读性差,可维护性差,容易误用
- 没有类型安全的检查 。
C++有哪些技术替代宏?
- 常量定义 换用const enum
- 短小函数定义 换用内联函数
6 auto关键字(C++11)
自动推导类型
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
现阶段用处一————范围for
正常我们的遍历
for(int i=0;i<sizeof(arr)/sizeof(int);i++)
{
}
范围for 遍历
for(auto e:a)
{
}
for(auto& e:a)
{
}
for(auto* e:a)//不能用指针,因为a是去数组的值而不能直接赋值给指针,有人会说&a啊,但是范围for不允许这样做!
{
}
它会自动迭代自动判断结束!
这个现在了解即可,后面会详解原理!
1.在同一行定义多个变量## auto不能使用的场景
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
2.auto不能作为函数的参数(编译的时候不知道开辟多大空间)
void TestAuto(auto a)
{}
3.auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
7.指针空值nullptr关键字(C++11)
7.1C++98中的指针空值
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖.
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
总结C++中的NULL和nullptr的区别
NULL就是指常量0,而nullptr是将0强转为地址(void*)0