首先我们要了解为什么会有C++的出现?
之所以要有C++,是因为C语言存在一定的缺陷,为了弥补这些缺陷,才出现了C++,除此之外,在C++中还引入了新的语法特性。下面简单介绍一下:
1. C++关键字
学习一门新的语言,都要从一个一个字开始,所以我们先来了解一下C++中的关键字。
在C++98中,有63个关键字,如下表:
这里并不对这些关键字一一介绍,在日常的学习中会慢慢接触到。
2. 命名空间
在C/C++中,完成一个项目时,必定会有大量的变量、函数,而这些变量和函数都是全局变量,所以难免存在命名冲突,如果为了避免命名冲突就去把变量名(函数名)给的很复杂,就会给之后的维护带来很大的麻烦。
而命名空间的出现很好的解决了这个问题,它的关键字namespace
,命名空间实际上就是一个新的作用域,以避免命名冲突或命名污染。
2.1 定义
在定义命名空间时,首先要有关键字namespace
,后面跟你要对这段空间起的名字,然后是{}
,{}
里面的内容就是命名空间的成员。示例:
- 普通的命名空间
namespace N1
{
int a = 10;
int add(int left, int right)
{
return left + right;
}
}
- 嵌套式命名空间
namespace N1
{
int a = 20;
int sub(int left, int right)
{
return left - right;
}
namespace N2
{
int a = 30;
int mul(int left, int right)
{
return left * right;
}
}
}
- 允许重名的命名空间
//当发生重名时,编译器会自动合成多个空间为一个
namespace N1
{
int mul(int left, int right)
{
return left * right;
}
}
注:
一个命名空间在定义完一段作用域以后,这个命名空间中的所有内容都被局限于该命名空间之中。
2.2 使用
命名空间的有三种使用方式:
- 使用命名空间名称及作用域限定符(推荐使用)
int main()
{
printf("%d\n", N::a);
return 0;
}
- 使用using引入命名空间中的成员
using N::a;
int main()
{
printf("%d\n", a);
return 0;
}
- 使用using namespace 命名空间名称引入(最不推荐使用,在预处理阶段会展开,增加代码量,降低效率)
using namespace N;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
add(10, 25);
return 0;
}
3. C++中的I/O
在C语言中,有很多的输入/输出函数,常用的printf
,scanf
,以及gets
,puts
等等,但在C++中使用cin
,cout
,这两个函数相比C语言中的输入/输出函数更为简便,不需增加数据格式控制。例如:整形–%d,字符–%c。
示例:
#include<iostream>//头文件
using namespace std;//std--标准命名空间
int main()
{
int a = 10;
char b = 'b';
double c;
cin >> c;
cout << a << " " << b << " " << c << endl;
return 0;
}
显示结果:
注:
在使用cout
标准输出(控制台)和cin
标准输入(键盘)时,必须包含<iostream >
头文件以及std
标准命名空间。
4. 缺省参数
4.1 概念
缺省参数是在声明或定义函数时就为函数的参数指定一个默认值,在调用该函数时,如果没有指定实参则使用该默认值;否则使用指定的实参。
示例:
void fun(int a = 1)
{
cout<<a<<endl;
}
int main()
{
fun();//不传参时打印1
fun(10);//传参时打印10
return 0;
}
4.2 分类
1).全缺省参数
示例:
void fun(int a=1,int b=2,int c=3)
{
cout<<a<<endl;
cout<<b<<endl;
cout<<c<<endl;
}
2).半缺省参数
示例:
void fun(int a,int b=2,int c=3)
{
cout<<a<<endl;
cout<<b<<endl;
cout<<c<<endl;
}
注:
- 半缺省参数的参数必须从右往左依次给出,不能间隔给出;
形参向实参传递时,从左往右依次传递 ;- 缺省参数不能在函数声明和定义中同时出现(如果同时出现,而恰巧声明和定义提供的值不同时,编译器会无法确定到底该用那个缺省值);
例如:
int fun(int a=10); //函数声明
int fun(int a=10) {return a;} //函数定义
缺省参数可以在声明中,也可以在定义中,但最好在声明中给出- 缺省值必须是常量或者全局变量 ;
- C语言不支持(编译器不支持)。
5. 函数重载
5.1 概念
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或参数类型的顺序)必须不同
,常用来处理实现功能类似数据类型不同的问题。
示例:
short add(short a, short b)
{
return a + b;
}
int add(short a, short b)
{
return a + b;
}
上例属于函数重载吗?
显然,它不属于函数重载 ,因为两个函数的参数列表完全相同,不同的只有返回值类型。
注:
- 在C++中,有四种作用域,分别是全局、局部、类域、命名空间;
- 函数重载中对返回值类型没有要求。
5.2 名字修饰
名字修饰实际上是一种在编译过程中,将函数、变量的名称重新改编的机制。简单来说就是编译器为了区分各个函数,将函数通过某种算法,重新修饰为全局中唯一存在的名称。
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
预处理:去注释、宏替换、包含头文件、处理预处理指令;
编译:把代码转换成汇编语言、词法分析、语法分析、语义分析、优化处理;
汇编:把汇编语言翻译成机器指令,生成目标文件;
链接:把所有的目标文件进行链接,生成可执行文件。
C语言的名字修饰规则非常简单,示例:
上述add
函数只给了声明没有给定义,因此在链接时就会报错。
从报错结果中可以看出,C语言的名字修饰规则只是简单的在函数名前加下划线。因此当工程中存在相同函数名的函数时,就会产生冲突。
而C++因为要支持函数重载、命名空间等,所以其名字修饰规则相对复杂,不同编译器在底层的实现方式可能都不同(这里是VS2017)。
通过VS中显示的错误可以看出,编译器在底层使用的并不是 add
名字,而是被重新修饰过的一个比较复杂的名字,被重新修饰后的名字中包含了函数的名字以及参数类型。
这就是函数重载中几个同名函数要求其参数列表必须不同的原因:
只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包含在最终的名字中,就可以保证函数名在底层的全局唯一性。
5.3 extern"C"
如果在C++的工程中,需要C风格的函数,只需要在函数名前加上extern"C"
即可,这个关键字是告诉编译器将该函数按照C语言规则来编译。
此时,链接后就变成了C风格的命名修饰规则。
6.引用
由于引用的内容相对较多,也比较重要,为了读者有一个良好的阅读体验,此处将引用的知识点单独列出一篇博客:引用
7. 内联函数
7.1 概念
用inline
关键字修饰的函数叫做内联函数,在编译时C++编译器会在调用内联函数的地方展开,并没有函数压栈的开销,所以内联函数的程序运行的效率比较高。
这是没有inline
关键字修饰的情况:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
return 0;
}
查看反汇编:
这里明显可以看到调用add
函数,这就势必要考虑函数压栈的开销,而这样就会降低效率。
然后再来看一下使用了inline
关键字以后它的反汇编代码,要在VS2017中使用inline
关键字,需要对编译器做一点修改。如下:
对编译器修改完以后(在完成了这次操作以后,记得将VS的设置修改回去),再次查看反汇编:
加了inline
关键字以后,函数就会变成内联函数,在编译期间编译器会用函数体替换函数的调用。
7.2 特性
inline
是一种以空间换时间的做法,省去调用函数的额外开销,所以当代码很长或者有循环/递归的函数不适宜使用内联函数;inline
对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline
的函数体内有循环/递归,编译器在优化时会忽略掉内联关键字;inline
不建议声明和定义分离,分离会导致链接错误。因为inline
被展开以后,就没有函数地址了,链接无法找到。
8. auto
关键字(C++11)
8.1 auto
简介
auto
在之前的C/C++版本中是一个存储类型的指示符,但在(C++11) 中,它是作为一个新的类型指示符来指示编译器,auto
声明的变量必须由编译器在编译时期推导而得。
int test()
{
return 5;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'c';
auto d = 1.2;
auto e = test();
auto e;
return 0;
}
编译结果如下:
通过这个我们可以知道:在使用auto
关键字定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto
的实际类型。auto
本身并不是一种“类型”,而是在类型声明时作为一个占位的作用(比如208中的“0”),编译器会在编译期间将auto
替换为变量实际的类型。
8.2 使用规则
1). auto
与指针和引用一起使用
使用auto
声明变量,声明指针类型是使用auto
和auto*
没有区别,但声明引用类型时,必须加上&
。
示例:
int main()
{
int a = 10;
auto&c = a;
auto d= 'a';
auto *e = &d;
cout << typeid(a).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(e).name() << endl;
return 0;
}
结果如下:
2). 在同一行定义多个变量
当想在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器会报错。实际上编译器只对第一个变量的类型进行推导,然后用推导出来的类型定义同一行的其他变量。
示例:
int main()
{
int a = 10;
auto b = a, c=1.3;
auto d = 10, e = 20;
return 0;
}
编译结果如下:
当想在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器会报错。实际上编译器只对第一个变量的类型进行推导,然后用推导出来的类型定义同一行的其他变量。
8.3 不能使用的场景
1).
auto
不能作为函数的参数( 因为编译器无法对a的实际类型进行推导 )
2).auto
不能直接用来声明数组
3). 为了避免与C++98中的auto
发生混淆,C++11中只保留了auto
作为类型指示符的用法
4).auto
在实际中常见的优势用法是和C++11中提供的基于范围的for
循环,lambda
表达式等进行配合使用
5).auto
不能定义类的非静态成员变量
6). 实例化模板时不能使用auto
作为模板参数
9. 基于范围的for
循环(C++11)
9.1 语法
先来看一下C++98中是怎样来实现for
循环的?
int main()
{
int array[] = { 1,2,3,4,5,6 };
for (int i = 0; i < sizeof(array) / sizeof(int); i++)
cout << i << endl;
return 0;
}
而在这段代码中,我们花了很多时间去算数组的大小。事实上,对于一个有范围的集合而言,由程序员自己来说明循环的范围是多余的,有时候还会出错。因此C++11中引入了基于范围的for
循环。
for
循环后的括号由冒号“ :”分为两部分:
第一部分是范围内用于迭代的变量, 第二部分则表示被迭代的范围。
示例:
int main()
{
int array[] = { 1,2,3,4,5,6 };
for (auto& e : array)
cout << e << endl;
return 0;
}
使用C++11中的循环方法后,可以很明显的发现代码量变少了很多。
9.2 使用条件
1).for
循环迭代的范围必须是确定的:
对于数组而言,就是数组中第一个元素到最后一个元素的范围;
对于类而言,应该提供得到 begin
和end
的 方法,begin
和end
就是for
循环迭代的范围。
2). 迭代的对象要实现++和==的操作。
10. 指针空值(C++11)
10.1 C++98中的指针空值
作为一个优秀的程序猿,声明一个变量给该变量一个合适的初始值是很有必要的,否则可能会出现难以预计的错误,比如未初始化的指针。C++98中我们经常使用NULL
来完成这个任务,而NULL
实际是一个宏,不信你看:
int main()
{
int *p = NULL;
return 0;
}
对NULL
转到定义发现:
可以看到,NULL
要么被定义为字面常量0,要么被定义为无类型的指针(void*)
的常量。不论使用哪一种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void test(int)
{
cout << test() << endl;
}
void test(int *)
{
cout << test(int *) << endl;
}
int main()
{
test(0);
test(NULL);
test((int *)NULL);
return 0;
}
编译结果如下:
可以看到,不但有一些调用过程中出现的错误,还有一些语法错误,而事实上这些语法错误是不存在的。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)
常量,但是编译器默认将其看成是一个整形常量,如果要将其按照指针方式来使用,则必须对其进行强转(void *) 0
。
10.2 nullptr
与nullptr_t
为了考虑兼容性,C++11并没有消除常量0的二义性,而是给出了全新的nullptr
表示空值指针。
C++11之所以不在NULL
的基础上进行扩展,是因为NULL
本身是一个宏,而且不同的编译器对于NULL
的实现可能不同,如果直接扩展NULL
,可能会影响以前的程序。
故此:为了避免混淆,C++11提供了nullptr
,即nullptr
代表一个指针空值常量。nullptr
是有类型的,其类型为nullptr_t
,仅仅可以被隐式转化为指针类型,nullptr_t
被定义在头文件:
typedef decltype(nullptr) nullptr_t;
注意:
1). 在使用nullptr
表示指针空值时,不需要包含头文件,因为nullptr
是C++11作为新关键字引入的。
2). 在C++11中,sizeof(nullptr)
与sizeof((void*)0)
所占的字节数相同(都占4个字节)。
3). 为了提高代码的健壮性,在后续表示指针空值时建议好使用nullptr
。
以上是对C++的一些简单说明,C++天下第一,它的深度远远不是一篇博客就可以得出的,希望大家多读书多看报,少刷网剧多刷代码!!!!