目录
前言
在前面学习了 C 语言和数据结构基础之后,终于要开始学习新篇章——C++ 了。我个人觉得前面一段时间的努力是为现在的学习做铺垫,有了之前的基础,我相信在学习 C++ 的道路上会走的更快、更扎实,下面就让我们走进通向 C++ 学习的大门吧。
关键字
在 c 语言中我们学习了 32 个关键字,而 C++ 在兼容 C 语言的基础上多出了不少关键字,但是我们并不需要将他们背下来,因为在学习的过程中我们会不断的接触到它们,遇到不会的就去查阅资料,慢慢的就可以真正的掌握它们。下面我列出所有的关键字:
命名空间
在 C / C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,这样一来可能会导致很多冲突。而使用命名空间的目的则是对标识符的名称进行区域化,以避免命名冲突或名字污染。
namespace
定义命名空间,需要使用到namespace
关键字,语法格式为:
namespace 空间名字 {
//变量
//函数
...
}
具体的定义格式有三种,展示如下:
//普通
namespace n1 {
int a = 10;
int fun(int m, int n){
return m > n ? m : n;
}
}
//嵌套
namespace n2 {
int a = 10;
int fun(int m, int n) {
return m > n ? m : n;
}
namespace n3 {
int b = 20;
int fun(int m, int n) {
return m > n ? n : m;
}
}
}
//分离:同一个项目中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中;
namespace n1 {
int c = 30;
int fun1(int m, int n) {
return m > n ? n : m;
}
}
使用方式
在知道如何定义命名空间之后,下面来介绍一下该如何使用命名空间中的成员,首先我们先定义一个命名空间,然后具体的使用方法有三种:
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;
}
}
int main(){
//printf("%d\n", a); 若直接使用命名空间中的变量,则该语句编译出错,无法识别 a
return 0;
}
-
- 加命名空间名称及作用域限定符
int main(){
//命名空间名称::成员
printf("%d\n", N::a);
return 0;
}
-
- 使用 using 将命名空间中成员引入
//在使用前将命名空间中的成员拿出,后面使用的时候就可直接使用
using N::b;
int main(){
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
-
- 使用 using namespace 将命名空间名称引入
//方法二是将空间中某一成员拿出,而方法三是将空间完全展开,该空间里面的成员都可直接使用,不过不建议大家使用这种方式
using namespce N;
int main(){
printf("%d\n", N::a);
printf("%d\n", b);
Add(10, 20);
return 0;
}
起别名
- 有时候命名空间太长,但是又不想展开,我们可以为命名空间起别名;
namespace 新名字=旧命名空间;
输入&输出
首先 C 语言中我们在输入输出时,需要包含<stdio.h>
头文件才可以正常进行,而在 C++ 中则是不同的,我们需要包含另一个头文件——<iostream>
,这样才可以进行正常的输入输出。另外要说明的一点是,在 C++ 标准库中所有的功能均在 std 标准命名空间下,所以我们在使用任何 C++ 库中的东西时,都需要前缀std::
,这样比较麻烦,此时我么就可以使用上面所说的第三种方法,在头文件下方加上using namespace std;
,这样我们就可直接使用标准库中的东西了。
在 C++ 中的输入输出和 C 语言中的输入输出是有较大差别的,在 C 语言中进行输入输出时,我们需要增加数据格式控制,比如:整形——%d,字符——%c;而在 C++ 中进行输入输出时,我们只需写成如下格式即可:
#include <iostream>
using namespace std;
int main(){
int a;
double b;
char c;
//输入一个数据给 a
cin >> a;
//连续输入,先输入一个数据给 b,再输入一个数据给 c
cin >> b >> c;
//输出 a,并换行;c 语言中的换行是'\n',而在 C++ 中则是'endl'
cout << a << endl;
//连续输出,输出 b,输出空格,输出 c,并换行
cout << b << " " << c << endl;
return 0;
}
其实输入输出的格式还有很多种,这里我就不一一列举了,因为大部分都用不上,若果有感兴趣的,可以看看这篇博客c++简单格式化输出输入。
缺省参数
缺省参数是指声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参,举例如下:
void TestFunc(int a = 0){
cout << a << endl;
}
int main(){
TestFunc(); // 没有传参时,使用参数的默认值
TestFunc(10); // 传参时,使用指定的实参
return 0;
}
缺省参数分类
- 全缺省参数:所有参数都有默认值;
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 = 20);//正确
void TestFunc(int a = 10, int b, int c = 20);//错误
void TestFunc(int a = 10, int b = 20, int c);//错误
- 缺省参数不能在函数声明和定义中同时出现;
//下面这个就是错误的,因为在声明和定义中同时给缺省值赋默认值,哪怕默认值一样也不行
int func(int a = 1);
int func(int a = 1) {
return 0;
}
//在声明和定义时,同一个缺省值只能在一个地方赋默认值
函数重载
概念
函数重载是函数的一种特殊情况,C++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表 (参数个数 或 类型 或 顺序) 必须不同,常用来处理实现功能类似数据类型不同的问题,但是要注意的是,C++ 中无法通过返回值类型不同来进行重载;举个例子:
int Add(int left, double right){
return left+right;
}
//与第一个函数参数个数不同
int Add(int left, double right, double mid){
return left+right;
}
//与第一个函数参数类型不同
int Add(double left, char right){
return left+right;
}
//与第一个函数参数顺序不同
int Add(double left, int right){
return left+right;
}
//这个就会报错,因为返回值类型不同,不能重载
double Add(int left, double right){
return left+right;
}
重载原理
为什么 C 语言中不能重载,而在 C++ 中可以重载呢?这是因为在 C 编译器和 C++ 编译器中底层对函数的命名规则不同,也就是说当我们写了一个函数之后,在 C 或 C++ 编译器中对与我们写的这个函数,编译器会有自己的命名规则;下面我来给大家演示一下,由于 vs 的修饰规则过于复杂,而 Linux 下编译器的修饰规则简单易懂,所以我们使用 Linux 下的 gcc 和 g++ 来给大家进行演示:
我相信通过上面的演示,大家就能理解了 C 语言没办法支持重载,因为同名函数没办法区分。而 C++ 是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载;另外这也解释了为什么在 C++ 中不能通过返回值类型不同来进行重载,因为在命名规则中没有标识返回值的信息,所以不可以。
extern
关键字
有时候在 C++ 项目中可能需要将某些函数按照 C 的风格来编译,那么我们可以在函数前加extern "C"
来解决,意思是告诉编译器,这个函数将按照 C 语言规则来编译,举个例子:
extern "C" int Add(int left, int right){
return left + right;
}
int main(){
//此函数将按照 C 语言规则来编译
Add(1,2);
return 0;
}
//有时我们需要将很多函数来进行 C 语言规则来编译,那么在前面一个一个加 extern "C" 太麻烦了,所以我们可以进行如下操作:
extern "C" {
//此时在这里面写的函数都将会按照 C 语言规则来编译
}
extern
可以用来声明全局变量,extern
声明之后就可以扩大全局变量的作用域,本来全局变量的作用域是从定义处开始直到文件结束,使用extern
提前声明之后就变成从声明处开始,直到文件结束;另外如果在声明时并赋值,那么就是定义而非声明了;
int main(){
//声明之后,变量i的作用域从该处开始直到文件结束。
extern int i;
//这是定义而非声明
extern int j = 0;
cout<<i<<endl;
return 0;
}
int i=5;
一个项目中不同文件想要使用同一全局变量,那么除了包含头文件的方法外,我么也可以使用extern
来声明之后使用,但是需要注意,如果是一个const
变量,那么在定义的地方也要在前面加上extern
才能使用;
file1:
int i =5;
//const修饰的变量这里必须加上extern才能在别的地方声明之后使用
extern const int j = 1;
file2:
//声明之后就可以使用了
extern int i;
extern const int j;
引用
什么是引用?通俗一点来说,引用就是给一个变量起别名、起外号,所以不管是用他的本名、别名还是外号来进行操作,最终都是作用在这个变量上的,这就是引用的含义;具体使用起来也很简单,语法格式为:变量类型& 引用变量名(对象名) = 引用实体
;
void TestRef(){
int a = 10;
//ra 引用 a,不管是使用 ra 还是 a 都是在操作同一片空间
int& ra = a;
//此时直接修改 ra,最终都会作用在 a 上的
ra = 20;
//此时打印出的结果为 20
cout << a << endl;
}
底层实现和语法层面
底层实现
引用说白了其实就是被包装过的指针,下面我们来看一段代码:
//用 ra 引用 a,并使用 ra 来修改 a 的值
int a = 10;
int& ra = a;
ra = 100;
//用指针 ra1 指向 a1,并使用 *ra1 来修改 a1 的值
int a1 = 10;
int* ra1 = &a1;
*ra1 = 100;
上面这一小段代码在运行之后,我们重点来看它的底层汇编实现,如下图所示,我们来进行横向对比,就会发现,在底层其实引用的指令和指针的指令一模一样,我们可以得出结论,在底层实现上,引用的本质就是指针,只不过系统内部自动执行了解引用的操作,我们在使用的时候可以省略掉这一步骤;
语法层面
首先我们来看下面这一端代码,他最终得到的结果是:相同;此时我相信很多小伙伴就有疑问了,上面明明说的是,引用的本质就是指针,所以 a 有自己的空间,ra 也有自己的空间,只不过 ra 中的内容是 a 的地址而已,为什么得到的结果会是相同呢?
void TestRef(){
int a = 10;
//ra 引用 a,ra 和 a 是同一块内存空间,不管是使用 ra 还是 a 都是在操作同一片空间
int& ra = a;
if(&a = &ra){
cout << "相同" << endl;
}
else{
cout << "不同" << endl;
}
}
其实啊,我给大家画一幅图,相信大家就明白了,如如下所示,通过这幅图和上面的解释说明,我相信大家一定搞明白了引用和指针的关系,所以一定不要搞混了哦。
引用的特性
- 引用在定义时必须初始化;
- 一个变量可以有多个引用;
- 引用一旦引用一个实体,不能再引用其他实体;
void TestRef(){
int a = 10;
int b = 20;
//引用在定义时必须初始化,所以下面这条语句编译时会出错
// int& ra;
//一个变量可以有多个引用,所以 ra、rra 都是 a 的引用
int& ra = a;
int& rra = a;
//引用只能引用一个实体,所以下面这条语句不是在引用 b,而是在将 b 的值赋给 ra
ra = b;
}
常引用
在定义引用时,在其前面加const
,此时该引用就会变成一个常引用,就和指针里的常量指针是一个意思,所用是被引用实体的值不能改变,主要出错的地方会是下面的这几种情况:
- 对常量做引用时,要加
const
; - 对常数做引用时,要加
const
; - 作引用时发生隐式转换,在隐式转换的过程中,会用到临时空间来保存常量,此时引用的就是该临时空间的常量,所以要加
const
;
void TestConstRef(){
const int a = 10;
//int& ra = a; 该语句编译时会出错,a 为常量
const int& ra = a;
// int& b = 10; 该语句编译时会出错,10 是常数,是一个常量,所以要加 const 才行
const int& b = 10;
double d = 12.34;
//int& rd = d; 该语句编译时会出错,d 在引用时,因为类型不同会导致隐式转换,此时会存在常量,必须加 const 才行
const int& rd = d;
}
使用场景
做参数
引用传参和指针传参效果是类似的,形参发生改变,实参也会改变,最常用的就是交换函数,如下图示;在之前我们知道值传递和址传递是有很大区别的,址传递不浪费空间、效率高,但是肯定有小伙伴觉得指针用起来不好用,很容易搞混淆,那么后面遇到这种情况我么就可以使用引用来解决,非常的方便,大家一定要记住哦。
void Swap(int& left, int& right){
int temp = left;
left = right;
right = temp;
}
做返回值
做返回值时,一定要注意返回值的作用域,如果出了作用域之后,该引用仍然存在,那就是可行的,否则就是不可行的;
内联函数
概念
以inline
修饰的函数叫做内联函数,编译时 C++ 编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
特性
inline
是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。inline
对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline
的函数体内有循环/递归等等,编译器优化时会忽略掉内联。- 定义在 class 声明内的成员函数默认是
inline
函数; - 在同一个项目的不同源文件内定义函数名相同的
inline
函数时,这些同名函数的实现必须相同,所以一般把inline
函数的定义放在头文件中更加合适,放在头文件中必须加inline
。 inline
不建议声明和定义分离,分离会导致链接错误。因为inline
被展开,就没有函数地址了,链接就会找不到(较新的编译器应该可以完成链接)。例如如下操作就会导致错误发生:
// F.h
#include <iostream>
using namespace std;
//函数声明
inline void f(int i);
// F.cpp
#include "F.h"
//函数定义
void f(int i){
cout << i << endl;
}
// main.cpp
#include "F.h"
int main(){
//调用函数
f(10);
return 0;
}
宏与内联
-
1、宏是实现了简单的替换,虽然有一些优点,但是缺点也很明显,比较如下
-
宏的优缺点:
- 优点:
1.增强代码的复用性。
2.提高性能。 - 缺点:
1.不方便调试宏(因为预编译阶段进行了替换)。
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
2、按照我们现在所掌握的知识,我们是有办法将宏来替代的,保留它的优点,去除缺点,主要是以下两种方法:
- 优点:
-
1.常量定义:换用
const
; - 2.函数定义:换用内联函数;
auto
关键字
概念
C++11 中,标准委员会赋予了auto
全新的含义即:auto
不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto
声明的变量必须由编译器在编译时期推导而得。
int TestAuto(){
return 10;
}
int main(){
int a = 10;
//推导 b 为 int 型
auto b = a;
//推导 c 为 char 型
auto c = 'a';
//推导 d 为 int 型
auto d = TestAuto();
//auto e; 无法通过编译,使用 auto 定义变量时必须对其进行初始化
//下面是语句打印变量的类型
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
注意:使用auto
定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto
的实际类型。因此auto
并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto
替换为变量实际的类型,如果没有初始化,编译器将会无法判断其类型。
使用细则
auto
与指针和引用结合起来使用,在用auto
声明指针类型时,用auto
和auto*
没有任何区别,但用auto
声明引用类型时则必须加&
,举个例子将会更好理解:
int x = 10;
//下面 px1 和 px2 都是一样的,都是指向 x 的指针
auto px1 = &x;
auto* px2 = &x;
int y = 10;
//ry1 引用 y
auto& ry1 = y;
//这里 ry2 不做引用,相当于是 ry2 等于 y,所以类型为 int
auto ry2 = y;
- 在同一行定义多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto(){
//正确,类型为 int
auto a = 1, b = 2;
//该行代码会编译失败,因为 c 和 d 的初始化表达式类型不同
auto c = 3, d = 4.0;
}
auto
不能作为函数的参数;
// 此处代码编译失败,auto 不能作为形参类型,因为编译器无法对 a 的实际类型进行推导
//其实就是相当于没有初始化
void TestAuto(auto a)
{}
auto
不能直接用来声明数组;- 如想了解的更多,可以看看其他大佬的博客,例如:C++11特性:auto关键字…
范围for
循环
语法
之前我们写打印数组循环的时候都是通过下面的方法来进行的:
void TestFor(){
int array[] = { 1, 2, 3, 4, 5 };
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
循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,具体演示如下:
void TestFor(){
int array[] = { 1, 2, 3, 4, 5 };
//使用 auto 是方便我们写代码,不用去管数组中数据的类型,系统自动判定
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
cout << endl;
return 0;
}
在上面的第一个for
循环这里,使用引用接受数组数据,不仅使得我们可以修改数组内容,还让程序效率提高,因为没有额外空间开销;如果想要既提高效率又不会改变数组内容,那么我们可以在auto
前面加const
,这样就可以实现了。
需要注意的是,如果数组作为参数传入函数,再在函数中使用范围for
循环的换,是不可以的,因为数组作为参数,会隐式转换为指针,这样就会丢失范围,系统无法帮我们界定操作开始和结束。
指针空值
给大家看一个代码,大家来判断一下应该是什么结果:
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(int),f(int),f(int*)
,由此可以看出,以前我们所接触到的NULL
,和整型 0 没什么区别,我们查看代码头文件可以得到如下结果:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
由上可知,NULL
并不是一个真正的空指针,在 C 语言中是将 0 强转为(void*)
,而在 C++ 中则是字面常量 0(C++ 中不能将void*
类型的指针隐式转换成其他指针类型),所以为了避免出现一些错误生成、为了避免混淆,在 C++11 中新定了一个关键字nullptr
,用于标识空指针,这是std::nullptr_t
类型的变量,它可以转换成任何指针类型和bool
布尔类型(主要是为了兼容普通指针可以作为条件判断语句的写法),但是不能被转换为整数,在今后的学习中,更建议大家使用这个,以此来规范我们代码,提高代码的健壮性。