1.前言
2.C++关键字
3.命名空间
3.1.命名空间的定义
3.2.命名空间的使用
4.C++的输入和输出
5.缺省参数
5.1.缺省参数的概念
5.2.缺省参数的分类
5.3.缺省参数的注意点
6.函数重载
6.1.函数重载的概念
6.2.函数重载的注意点
6.3.为什么C++支持函数重载------名字修饰
6.4.小结
7.extern “C”
8.引用
8.1.引用的概念
8.2.引用的特性
8.3.常引用
8.4.引用做参数
8.5.引用做返回值
9.内联函数
9.1.概念
9.2.特性
10.auto关键字(C++11)
11.范围for
12.nullptr
相比于C语言的32个关键字,C++的关键字加至63个,很大程度上拓展了C语言的功能。
在C/C++中,变量,函数和类都是大量存在的。这些变量,函数,类都存在于全局作用域中,可能导致很多冲突。举个例子,我们的命名的变量跟库里的变量名,函数名冲突了。就会导致重命名的问题。在实际大型项目的开发中,还存在同事之间定义的变量/函数/类型命名冲突的情况。
比如说,在头文件
为了对标识符的名称进行本地化,以避免命名冲突和名字污染,namespace便应运而生了。
我们刚刚提到C++有63个关键字,几乎比C语言多了一倍。那么,我们就来看看其中新增的一个关键字——namespace吧!
定义命名空间,我们就需要用到namespace关键字,后面跟命名空间的名字。然后接一对{}即可。{}中包含的即为命名空间的成员。
比如说,我和小许合作开发一款社交软件,我们都在自己的文件里定义了全局变量cq。那在链接的过程中,就会报重命名的错误。于是,我们只好采用不同的命名空间来定义我们各自的cq变量啦!
我们在全局定义了命名空间C1和C2:
namespace C1 {
int cq = 30;
}
namespace C2 {
int cq = 80;
}
不仅如此,我们还可以在命名空间里定义其他任意类型的变量以及函数:
namespace n1 {
char ch;
int a;
double b;
struct book
{
char name[20];
int price;
int number;
};
int Add(int x, int y)
{
return x + y;
}
}
甚至还可以嵌套定义命名空间:
namespace n2
{
int a;
int b;
namespace n3
{
int Sub(int x, int y)
{
return x - y;
}
}
}
最后,同一个工程中允许存在多个相同名称的命名空间,编译器会最后合成到同一个命名空间中。
namespace C1 {
int cq = 30;
}
namespace C1 {
int cp = 40;
}
好了,我们定义了各自的命名空间里定义了各自的cq,那用的时候怎么引用cq变量呢?
今有命名空间N
namespace N {
int a = 30;
int b = 40;
int c = 50;
}
我们有三种方法来使用命名空间N中定义的变量
using namespace
直接将命名空间中的变量全部展开到全局using namespace N;
优点:无脑方便
缺点:把定义的变量暴露出去了,容易造成命名污染
命名空间名称
+域操作符 ::
在访问时指定命名空间cout << N::rand <<endl;
优点:不存在命名污染
缺点:用起来太烦了
using
将部分命名空间成员展开using N::a;
using N::b;
这样既降低了命名污染的概率,又能在使用变量时偷懒了
初识一门新语言,我们按老规矩办事!
#include
using namespace std;
int main()
{
cout << "hello world!" << endl;
return 0;
}
于是,hello world!
便打印在了控制台中了!
我们来认识一下C++的输入输出运算符
cin >> n;
cout << a[i];
注意:
cout<< "hello" << " " << "world" << "!" << endl;
备胎,就是给汽车准备一个备用轮胎,一旦那个轮子爆胎或者出了问题,备用轮胎就方便及时地取而代之,汽车就不至于中途抛锚。
顾名思义,这“感情备胎”就是给自己在感情的归宿上像轮胎一样,有多一个甚至多个备份,“感情备胎”一般多指爱情。
悄悄告诉你,C++的函数参数
也有备胎哦!
缺省参数是指在声明或定义函数时给函数的参数指定一个默认值
。
在调用该函数时,如果没有指定实参,就采用这个默认值,否则就采用实参。
比如说,我们希望malloc/realloc有默认开辟的大小。在希望使用默认值的时候缺省参数,不希望的时候传一个自己想要的参数。
我们拿出以前写过的栈的数据空间的初始化以及增容的代码:
void StackBuy(Stack* ps, int init_num = 4)
{
assert(ps);
ps->capacity = ps->capacity == 0 ? init_num : 2 * ps->capacity;
StackDataType* tmp = (StackDataType*)realloc(ps->data, ps->capacity * sizeof(StackDataType));
if (tmp == NULL)
{
printf("realloc failed\n");
return;
}
ps->data = tmp;
}
这里,我们让栈的data的元素数量默认是4,但是如果你想要让它是8或者16的时候,多传一个参数就行了!
缺省参数可以分成全缺省参数和半缺省参数
void TestFunc(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
void TestFunc(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
void TestFunc(int a, int b = 10, int c)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
int a = 10
仅需在声明或定义的地方写一次就行了。常量
或者全局变量
。自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
学校期末考试考完了,出考场后:
学霸:“考试完了。”
学渣:“考试完了。”
嘿嘿,虽然这两句话读起来一样,但表达的意思可不一样哦!
C++里面也有一函数多义的情况!
函数重载:是函数的一种特殊情况,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 Add(int n1, int n2)
{
return n1 + n2;
}
int Add(int n1, int n2, int n3)
{
return n1 + n2 + n3;
}
int Add(int n1, double n2)
{
return n1 + n2;
}
int Add(double n1, int n2)
{
return n1 + n2;
}
请特别注意,返回值不同,函数名及参数相同的函数不能重载!
short Add(short left, short right)
{
return left + right;
}
int Add(short left, short right)
{
return left + right;
}
这就不属于函数重载
我们说函数重载是C语言不具有的功能,而C++添加了这个功能?
函数重载究竟是怎么实现的呢?
这就要从我们的底层函数调用讲起了
在C/C++中,一个程序要运行起来,分为预处理,编译,汇编,链接四个步骤
让我们尤其来关注链接
这一步
链接时,当一个文件f1的某一行代码调用了文件f2中的函数a时,编译器看到f1调用了a,但是f1中找不到a的地址,于是,编译器就去f2的符号表中寻找a的地址
,发现能够找到,然后便把a和f1链接到一起
那么编译器根据什么去寻找函数的呢?
答案是 通过编译后的函数名修饰
,对应函数的地址,通过地址,最后找到函数
我们在Linux下分别使用gcc和g++编译器看一下文件a.c和test.c
链接后的反汇编源码
我们来看C语言编译器gcc编译链接后的结果:
我们可以看到,gcc编译器的函数名修饰只与函数名本身有关而与函数参数无关,所以C语言不支持函数重载,因为即使重载了你也无法让编译器去通过参数的不同在链接时找到不同的函数!
我们可以看到,g++编译器通过函数名修饰规则将符号表中的函数符号修饰成了_Z3Addii
的形式
_Z3
代表函数名长度
Add
代表函数名
ii
代表有2个参数int和int
这样参数不同的同名函数在符号表中被修饰成了不同的符号,这就能在链接时轻松找到不同函数了!
通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。
而C++是通过函数修饰规则来区 分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
另外我们也理解了,为什么函数重载要求参数不同!而跟返回值没关系。
C++编译器能识别C++函数名修饰规则,也能识别C函数名的修饰规则
有的时候,在C++工程中可能需要将某些函数按C的风格编译,那么,我们只要在函数前面加 extern “C”
,意思是告诉编译器,将该函数按C语言规则来编译
请注意,extern “C”
要在函数声明以及定义的地方都加上,不能只加一个地方,否则就会报错!
先看一段代码:
int main()
{
int a = 10;
int& ra = a;
}
这里,ra就是a的引用。
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如,特朗普本名唐纳德·特朗普,但在中国,被叫做懂王,懂王就是他的别名
引用的格式如下:
类型& 引用变量名(对象名) = 引用实体;
int& ra;
int a = 10;
int& ra = a;
int& ra2 = a;
int& rra = ra;
int a = 10;
int& ra = a;
int b = 20;
ra = b;
ra = b;并不是让ra从a的引用变成b的引用,而是将b赋值给ra
首先我们提一句话:
引用时,别名的权限能够减小或者不变,但不能放大
void TestConstRef()
{
const int a = 10;
int& ra = a;
//a被const修饰,表示a不能被改;而ra是a的引用,ra并没有说明自己不能被改,这时ra的权限被放大了,所以编译报错
}
void TestConstRef()
{
const int a = 10;
const int& ra = a;
//这样写才是对的
}
void TestConstRef()
{
int& ra = 10;
//10是常量不能被修改,而ra却是可以被修改,权限被放大了
const int& ra = 10;
//这样写才是对的
}
void TestConstRef()
{
double d =1.234;
int& rd = d;
//这里涉及到整型提升,将double转换为int时,提升完了的值存放在一个具有常性的临时变量里,所以右边是常数,左边是变量,左边权限过大
const int& rd = d;
//这样写才是对的
}
所以引用前加const的好处是:
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
先看一段错误的代码
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
运行结果是7.
Why?
首先我们要知道函数被调用时会建立栈帧,函数被执行完时栈帧就会被销毁,那么返回值是怎么被从Add函数中返回到main函数中的呢?你不是说Add函数 执行完return c; 之后栈帧就被销毁了吗?c是怎么被传递出来的呢?哦,传值返回时,会为返回值开辟一个临时变量,这个临时变量还未还给操作系统,那么使用引用返回,就能把这块地址上的值返回出去。这还是很危险的,万一已经返还给操作系统了呢,内存已经被清空了呢?
于是,我们假设临时变量的地址为0x8822ff44,第一次该地址上的值被置为3
同时,ret对应地址0x8822ff44
第二次调用Add,0x8822ff44上的值又被改为7
于是,ret对应的值就是7了
在C++中,内联函数被用来替代宏函数
inline Add(int a, int b)
{
return a + b;
}
以inline修饰的函数叫内联函数,编译时C++编译器会在调用函数的地方展开,没有建立栈帧的开销,提升了程序运行的效率。
来看这样一段代码:
int a = 1;
char b = 'y';
auto c = a;
auto d = b;
auto关键字帮助我们通过右边的赋值,自动判断左边声明的变量的类型
优点:简化了代码
缺点:降低了代码的可读性
我们可以通过打印 typeid(变量).name() 来查看变量的类型
请注意:
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
}
int main()
{
auto a = 3, c =4.0;
}
void TestAuto(auto a)
{}
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
auto不能用来声明数组,因为编译器不知道要申请多少字节的空间,后面的 3 究竟是short还是int还是long long?
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。
因此C++11中 引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:
第一部分是范围内用于迭代的变量,
第二部分则表示被迭代的范围。
注意:for循环迭代的范围必须是确定的
int main()
{
int arr[] = { 1,2,3,4,5,6 };
for (auto e : arr)
cout << e << endl;
}
好的编程习惯应该是,在声明一个变量的时候,给它赋一个合适的初值。
在对指针进行初始化的时候,我们在C语言中常常这样做:
int p = NULL;
NULL在C语言里是空指针,值为0
,类型是(void*)
,代表地址0x00000000
而我们看到C的头文件
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
这段代码告诉我们,我们用C编译器,NULL就是0处的地址;用C++编译器,NULL就是一个字面常量0,没有指针的属性
以下情况下,NULL的使用就会产生麻烦:NULL被当作int 类型的0,而不是0x00000000
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;
}
于是,C++11引入了新关键字 nullptr
,代表0处地址
注意: