目录
- [一、C++ 在 C 上进行的优化](#一、C++ 在 C 上进行的优化)
- [二、C++ 关键字(C++ 98)](#二、C++ 关键字(C++ 98))
- [三、C++ 的输入输出](#三、C++ 的输入输出)
-
- [1. cin 和 cout 的使用](#1. cin 和 cout 的使用)
- [2. cin、cout 和 scanf()、printf() 的区别](#2. cin、cout 和 scanf()、printf() 的区别)
- 三、命名空间
-
- [1. 命名空间的使用](#1. 命名空间的使用)
- [2. 嵌套命名空间](#2. 嵌套命名空间)
- [3. 在多个头文件中使用相同的命名空间](#3. 在多个头文件中使用相同的命名空间)
- 四、函数缺省值
-
- [1. 缺省值的使用](#1. 缺省值的使用)
- [2. 缺省值使用的注意事项](#2. 缺省值使用的注意事项)
- 五、函数重载
-
- [1. 函数重载的使用](#1. 函数重载的使用)
- [2. 为什么函数返回类型不同不算函数重载?](#2. 为什么函数返回类型不同不算函数重载?)
- 六、引用
-
- [1. 引用的使用](#1. 引用的使用)
- [2. 常量引用](#2. 常量引用)
- [3. 常量引用和普通引用的区别](#3. 常量引用和普通引用的区别)
- [5. 引用传参](#5. 引用传参)
- [6. 引用的注意事项](#6. 引用的注意事项)
- 七、内联函数
-
- [1. 内联函数的使用](#1. 内联函数的使用)
- [2. 内联函数的注意事项](#2. 内联函数的注意事项)
- [3. 宏的优缺点?C++ 有哪些技术替代宏?](#3. 宏的优缺点?C++ 有哪些技术替代宏?)
- [八、auto 关键字](#八、auto 关键字)
-
- [1. auto 关键字的使用](#1. auto 关键字的使用)
- [2. auto 关键字使用的注意事项](#2. auto 关键字使用的注意事项)
- [九、基于范围的 for 循环](#九、基于范围的 for 循环)
-
- [1. 范围 for 的使用](#1. 范围 for 的使用)
- [2. 范围 for 的使用条件](#2. 范围 for 的使用条件)
- [十、空指针 nullptr(C++11)](#十、空指针 nullptr(C++11))
一、C++ 在 C 上进行的优化
C++ 在 C 的基础上加入了面向对象编程、泛型编程和许多有用的库。弥补了 C 语言的不足,并对 C 语言不合理的地方进行优化,使其更加适应当前时代的需求和发展。
由于 C++ 兼容大部分的 C,所以可以在 C++ 中混合使用 C 来进行编写代码,如使用 printf() 函数和 scanf() 函数。
二、C++ 关键字(C++ 98)
下面的关键字看看就行,不用背,用的多了就记住了,很多都是 C 语言学过的。
三、C++ 的输入输出
在 C++ 中,输入输出被看成输入流和输出流,使用 iostream 库中的 istream 和 ostream 分别表示输入流和输出流。标准输入就可以理解为把键盘敲击输入的信息放入输入流中,而标准输出就可以理解为把输出的信息放入输出流中,随着输出流显示到显示器上。一个流就是一个字符。术语 "流" 的意思是,随着时间的推移,字符是顺序生成或者消耗的。
而标准库定义了名为 cin 的 istream 类的对象和名为 cout 的 ostream 类的对象并通过流插入运算符(<<)和流提取运算符(>>)用来处理标准输入和标准输出。cin 和 cout 均可智能识别其后数据的类型。
1. cin 和 cout 的使用
首先,使用 cin 和 cout 输入输出对象需要完成下面两步:
1)包含 iostream 库,因为 istream 类和 ostream 类包含在 iostream 库中。
2) 使用标准命名空间 std,或者单独声明 cin 和 cout。
c
// 头文件
#include <iostream>
// 使用标准命名空间 std
using namespace std;
int main()
{
int i = 0;
double d = 0;
char c = 0;
// 输入
cin >> i; // 单个变量输入
cin >> d >> c; // 多个变量连续输入
// 输入
cout << "i = " << i << endl;
cout << "d = " << d << endl;
cout << "c = " << c << endl;
return 0;
}
在上述代码中,流提取运算符(>>)搭配 cin 使用,流插入运算符(<<)搭配 cout 使用,后面跟需要输入和输出的数据即可。也可以连续输入和输出,因为 cin 和 cout 在输入和输出完成第一个数据之后会返回一个类对象的引用。连续输入只要中间间隔空白(空格、制表符和换行符)即可。
endl 是 C++ 的操纵符,把它写入标准输出流的作用是换行并刷新缓冲区。
代码运行结果如下:
2. cin、cout 和 scanf()、printf() 的区别
1)cin、cout 是类对象,而 scanf() 和 printf() 是函数;
2)cin、cout 使用流插提取运算符(>>)和流插入运算符(<<)来进行输入和输出,scanf() 和 printf() 使用参数传递的方式来进行输入和输出;
3)cin、cout 适用于非格式化输入和输出,scanf() 和 printf() 适合格式化输入和输出。
三、命名空间
当我们包含库中的头文件时,由于该头文件中的内容过多,很容易和我们自己命名的名称冲突。当我们和他人一起编写大型项目时,我们和他人命名的名称也很容易冲突。
上面的问题放在 C 语言中,只能使用不同的名称,而在 C++ 中可以通过使用关键字 namespace 创建命名空间来解决。
1. 命名空间的使用
创建命名空间的格式为:namespace 名称 { ... }
cpp
// 头文件
#include <iostream>
// 使用标准名称空间
using namespace std;
// 创建命名空间 qcx
namespace qcx
{
int a = 3;
// 交换函数
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
}
// 交换函数
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int a = 2;
int main()
{
int a = 1;
cout << "局部变量: " << a << endl;
cout << "全局变量: " << ::a << endl;
cout << "命名空间: " << qcx::a << endl;
return 0;
}
上述代码运行结果如下:
可以看到,代码正常运行。且在上述代码中,出现了三个同名变量 a,和两个一模一样的函数,倘若这放在 C 语言中,编译阶段就已经报错了。
第一条 cout 语句中的变量 a 是局部变量,当全局变量名和局部变量名冲突时,局部优先,且编译器在使用变量名时,会先在局部查找,然后在全局查找,不会主动访问命名空间的内容。第二条 cout 语句中的变量 a,使用 C++ 中的作用域解析运算符(::),当该运算符的左侧为空白时,默认访问全局变量。第三条 cout 语句,使用作用域解析运算符(::)显示访问命名空间 qcx 中的变量 a。
名称冲突是在同一个作用域中出现了同名的变量或者函数,而命名空间的作用是开辟一个新的作用域,所以上述代码中的三个 a 变量的作用域分别为:局部作用域,全局作用域,命名空间作用域,所以不会造成命名冲突。
(1)为什么包含头文件 iostream 后还需要使用标准命名空间 std ?
在学习 C 语言的时候,包含的头文件在编译阶段就直接展开了,且该头文件包含的内容可以直接使用。
但是,C++ 把标准库中的内容全部封装到了标准命名空间 std 中,比如现在包含头文件 iostream 相当于包含了如下内容:
namespace std
{
iostream 文件的内容
}
就像我们前面说的,编译器是不会主动访问命名空间中的内容的,所以如果仅仅只包含该头文件,我们是不能去使用里面的工具的。
而语句 using namespace std; 的作用是把外面的 namespace std 去掉,把里面的内容显示出来,也就相当于学习 C 语言时的包含头文件。
但是,如果直接使用 using namespace std,不就相当于命名空间白用了?把头文件中的内容全部展开和 C 语言不就没有区别了?所以,C++ 还提供了把命名空间里面的单个工具展开,如:using std::cout;,把 cout 类对象放入全局,这样我们就可以使用 cout 工具,且不用担心名称冲突了。
当然如果不嫌麻烦,也可以显示访问命名空间 std,如:std::cout << qcx::a << std::endl;。
(2)命名空间中可以存放变量、结构体、函数、类等内容
2. 嵌套命名空间
嵌套命名空间就是一个命名空间里面再包含命名空间,和嵌套循环一个意思。这个很容易理解,比如我们制作一款游戏,那么把战斗模块和数值模块分的代码放入各自的命名空间,然后战斗模块的每个组的代码放入各自的命名空间,最后每个组的成员的代码又放入各自的命名空间。
具体效果类似如下代码:
cpp
// 游戏
namespace game
{
// 战斗模块
namespace Battle
{
// a 组
namespace a
{
// 小王
namespace wang
{
// 代码...
}
// 小张
namespace zhang
{
// 代码...
}
}
// b 组
namespace b
{
// 小王
namespace wang
{
// 代码...
}
// 小张
namespace zhang
{
// 代码...
}
}
}
// 数值模块
namespace Numerical
{
// a 组
namespace a
{
// 小王
namespace wang
{
// 代码...
}
// 小张
namespace zhang
{
// 代码...
}
}
// b 组
namespace b
{
// 小王
namespace wang
{
// 代码...
}
// 小张
namespace zhang
{
// 代码...
}
}
}
}
如果需要访问战斗模块 a 组小王的变量 age,可以使用如下方法:
1)using Game::Battle:: a::wang::age;
2)using namespace Game::Battle:: a::wang;
(上面 a 前面加个空格是CSDN防止格式化)
3. 在多个头文件中使用相同的命名空间
比如在头文件 head1 中创建命名空间 qcx,然后在头文件 head2 中创建命名空间 qcx,那么这两个命名空间会合并为一个命名空间。如果在这两个头文件的 qcx 命名空间中定义了相同的名称,编译器会报错。
四、函数缺省值
函数缺省值的作用是不给函数传递参数时,函数使用默认值。在创建顺序表时,通常我们需要传递一个参数来初始化顺序表的大小,但是有时又不确定这个值给多大,给大了浪费空间,给小了需要扩容。所以可以给一个缺省值。
1. 缺省值的使用
cpp
// 头文件
#include <iostream>
#include <stdlib.h>
// 使用声明
using std::cout;
using std::endl;
// 类型声明
typedef int DataType;
// 顺序表
struct SQList
{
DataType* pdata;
size_t size;
size_t capacity;
};
// 初始化顺序
void InitSQList(SQList* sl, int capacity = 2)
{
// 申请空间
sl->pdata = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == sl->pdata)
{
perror("InitSQList::malloc: ");
return;
}
// 初始化成员
sl->size = 0;
sl->capacity = capacity;
}
// 获得顺序表的容量
size_t GetCapacity(SQList* sl)
{
return sl->capacity;
}
int main()
{
// 创建顺序表
SQList s1, s2;
// 使用缺省参数
InitSQList(&s1);
// 传递参数
InitSQList(&s2, 8);
// 打印容量
cout << "s1 的容量:" << GetCapacity(&s1) << endl;
cout << "s2 的容量:" << GetCapacity(&s2) << endl;
return 0;
}
可以看到程序的运行结果如下:
2. 缺省值使用的注意事项
(1)函数声明填写缺省值,函数定义不写
这样做的好处是防止两边对不上,其次编译阶段需要检查函数声明,如果函数声明不写,函数定义写的话,编译阶段就会报错。
(2)函数缺省值需要从右往左依次填写
由于函数传参是从左到右依次传递的,所以函数缺省值需要从右往左依次缺省。因为C++在传递参数的时候不允许有参数为空。
(3)所有参数都有缺省值叫全缺省,部分参数有缺省值叫班缺省
五、函数重载
函数重载顾名思义就是在相同的作用域中可以存在两个相同名称的函数。这放在 C 中是不允许的,因为 C 中识别函数主要是通过函数名来识别的,而C++中加上了参数。
举个简单的例子,就拿函数 void Swap(int a, int b) 来说,在 C 语言中可能就是通过 Swap 来识别,但是在 C++ 中,是通过 Swapii,来识别,后面的 ii 代表了两个 int 参数。
所以,C++ 支持函数重载,只要函数的参数数量、类型和顺序不同即可。(上述只是简单说明,编译器具体的识别肯定不想上述这么简单)
1. 函数重载的使用
c
// 头文件
#include <iostream>
// 使用声明
using std::cout;
using std::endl;
// 交换函数
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void Swap(double* a, double* b)
{
double tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int a = 1, b = 2;
Swap(&a, &b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
double c = 1.1, d = 2.2;
Swap(&c, &d);
cout << "c = " << c << endl;
cout << "d = " << d << endl;
return 0;
}
程序运行结果如下:
&esmp; 可以看到通过使用函数重载,可以把一个函数的功能作用到不同类型的变量上。
2. 为什么函数返回类型不同不算函数重载?
如果两个函数仅仅只是函数返回值不同,那么在编译截断就会报错,由于函数调用语句一模一样,编译器不知道你想调用哪个函数。
六、引用
引用的作用是给已经存在的变量取一个别名,也就是引用变量和被引用的变量共用一块空间。
创建引用变量的格式为:类型 &引用变量名 = 被引用的变量名
1. 引用的使用
cpp
// 头文件
#include <iostream>
// 使用声明
using std::cout;
using std::endl;
int main()
{
// 创建变量 a
int a = 10;
// b 是对 a 的引用,也就是 b 是 a 的别名
int& b = a;
// 值验证
cout << "a = " << a << endl;
cout << "b = " << b << endl;
// 地址验证
cout << "&a = " << &a << endl;
cout << "&b = " << &b << endl;
return 0;
}
程序的运行结果如下:
可以看到上述变量 a 和 变量 b 的值和地址均相同,可以得出它们都是代指同一块空间。就好像一个人拥有外号,如李逵又叫黑旋风,但是叫的都是同一个人。而引用所代表的就是这种关系。如下图所示:
2. 常量引用
常量引用相比于普通引用的区别就是其值不能修改,类似 const 变量和普通变量。
cpp
// 头文件
#include <iostream>
// 使用声明
using std::cout;
using std::endl;
int main()
{
int a = 10;
// 普通引用
int& ra = a;
ra = 1;
cout << "a = " << a << endl;
return 0;
}
普通引用可以修改:
常量引用不能修改:
从上述代码以及运行结果中可以看出,常量引用不能修改只能读取,普通引用既能修改又能读取。
3. 常量引用和普通引用的区别
1)常量引用既能引用常量又能引用变量,而普通引用只能引用变量。
2)常量引用不能修改,普通引用可以修改
5. 引用传参
引用传参非常好用,首先引用传参效率高,因为直接使用原对象,其次引用传参节省空间,因为引用和引用对象共用一块空间。
下面是使用引用传参的 Swap() 函数:
cpp
// 头文件
#include <iostream>
// 使用声明
using std::cout;
using std::endl;
// 交换函数
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10;
int b = 20;
// 交换前
cout << "交换前:\n";
cout << "a = " << a << endl;
cout << "b = " << b << endl;
// 交换后
Swap(a, b);
cout << "交换后:\n";
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
代码运行结果如下:
可以看到引用传参比指针传参还要香,不仅传参直接传递,在函数中也可以直接使用。而指针传参需要取地址,在函数中还需要解引用。
而且引用还可以作为返回值,其他类型作为返回值都需要先拷贝到一个临时对象,然后再返回,而引用作为返回值是直接返回,提升了效率。但是引用作为返回值不能返回局部变量。
6. 引用的注意事项
1)引用必须在初始化时绑定对应类型的对象,且绑定之后不能转换绑定对象;
2)引用不开辟空间,和被引用对象共用一块空间;
3)普通引用只能绑定变量,常量引用没有限制;
4)函数不能返回局部变量的引用;
5)传参时如果不需要修改值,传递常量引用,避免不小心修改;
6)不要把引用和指针混淆。
七、内联函数
函数调用需要建立栈帧,但是如果该函数仅仅只有寥寥几行代码,这样每次调用该函数的开销主要在建立栈帧上了。
C 语言通过宏来解决上述问题,如下代码:
c
// 头文件
#include <iostream>
// 使用声明
using std::cout;
using std::endl;
// 宏声明
#define Add(x, y) (((x) + (y)) * 10)
int main()
{
cout << Add(10, 20) << endl;
return 0;
}
通过上述代码可以发现,虽然宏能解决这个问题。但是宏是在编译过程中直接替换的,也不进行类型检查,且写法复杂,又由于运算符优先级要在表达式的每个部分加上括号。
C++ 通过内联函数解决上述问题,直接在函数前面加上 inline 即可。内联函数的作用是,当运行到函数调用语句时,直接展开函数体,也就是用函数体替换函数调用语句。
1. 内联函数的使用
cpp
// 头文件
#include <iostream>
#include "head1.h"
// 使用声明
using std::cout;
using std::endl;
宏声明
//#define Add(x, y) (((x) + (y)) * 10)
int main()
{
宏声明
//cout << Add(10, 20) << endl;
// 内联函数
Add(10, 20); // 相当于 return (10 + 20) * 10;
return 0;
}
内联函数相当于在函数调用语句处展开函数体(带入参数值)。
2. 内联函数的注意事项
1)online 关键字只能添加在函数声明;
2)添加 online 关键字只是建议编译器让该函数成为内联函数,具体还需要编译器自行判断;
3)一般把内联函数直接定义在头文件中,其他文件需要使用只需要包含该头文件即可;
4)长度过长的函数和递归函数不适合成为内联函数,
5)内联函数只在 release 下起作用,在 debug 下不起作用;
6)内联函数不建议声明和定义分离,因为 inline 展开了,就找不到函数地址了,链接错误。
3. 宏的优缺点?C++ 有哪些技术替代宏?
(1)
优点:
1)针对不带参宏,提升了代码的可读性和可维护性;
2)针对带参宏,减少了函数栈帧的开销,提升了效率。
缺点:
1)缺少类型检查;
2)针对带参宏,可读性差,可维护性差,容易用错(由于运算符的优先级,必须在表达式的每个部分加上括号)
(2)
1)C++ 使用关键字 const 来定义常量;
2)C++ 使用内联函数替换带参宏。
八、auto 关键字
auto 关键字的作用是在创建变量的时候根据赋值表达式的类型推断出变量的类型。
当变量的类型复杂时,使用 auto 关键字非常方便且不易出错。
1. auto 关键字的使用
cpp
// 头文件
#include <iostream>
// 使用声明
using std::cout;
using std::endl;
int main()
{
auto a = 1; // int
auto b = 1.1; // double
auto c = 'a'; // char
auto d = "aaaaa"; // const char*
auto e = 1 + 1.1; // double
return 0;
}
从上述代码中的最后一条 auto 赋值语句中可以得出,auto 语句是根据表达式的值进行类型推断的。
2. auto 关键字使用的注意事项
1)在早期的 C/C++ 中,auto 关键字修饰的变量是具有自动存储器的局部变量。由于在函数中默认创建的变量就是局部变量,所以导致该关键字几乎没人使用。所以,C++11 标准赋予该关键字根据赋值表达式推断类型的能力。
2)使用 auto 关键字必须对变量初始化,因为 auto 关键字需要根据表达式的类型来推断该变量的类型。因此 auto 并非是一种类型声明,而是一个类型声明时的占位符,编译器在编译器会把 auto 替换为该变量对应的类型。
3)用 auto 声明指针类型时,使用 auto 和 auto* 没有任何区别,只是后者显示表示该类型是个指针。
4)用 auto 声明引用类型时必须使用 auto&。
5)使用 auto 在一行定义多个变量时,这些变量的基本类型必须相同。如:int、int*、int&,它们的基本类型都是 int。
6)auto 不能用来作为函数参数,也不能直接用来声明数组。
九、基于范围的 for 循环
范围 for 是 C++11 新增的一种遍历方法,for 后面的圆括号被冒号(:)分为两部分,前者是范围内用于迭代的变量,后者是被迭代的范围。
迭代的变量被依次赋予迭代范围中的值,所以正常情况下只能读取数据,就相当于把范围中的值依次拷贝到迭代的变量中。所以一般情况下使用引用,这样既减少了拷贝的环节,又可以修改范围中的数据。当只需要读取数据时,前面加上 const 即可。
1. 范围 for 的使用
cpp
// 头文件
#include <iostream>
// 使用声明
using std::cout;
using std::endl;
// 常量声明
const int SIZE = 10;
int main()
{
int arr[SIZE];
for (int i = 1; i <= 10; ++i)
arr[i - 1] = i;
// 使用范围 for 变量数组
for (auto& elem : arr)
{
cout << elem << " ";
elem *= 2;
}
cout << endl;
for (auto& elem : arr)
cout << elem << " ";
return 0;
}
代码的运行结果如下:
与普通的循环一样,可以使用 continue 或者 break 跳出循环。
2. 范围 for 的使用条件
1)迭代的范围必须是确定的,如数组就是第一个元素到最后一个元素;
2)迭代的对象要实现++和==的操作。(范围 for 的本质是迭代器)
十、空指针 nullptr(C++11)
在 C 语言中,空指针使用 NULL 表示。但是在 C++ 中,NULL 是一个宏,即 #define NULL 0。所以它的本质是数字 0,而在 C++ 中,关键字 nullptr 代表空指针。如下代码:
cpp
// 头文件
#include <iostream>
// 使用声明
using std::cout;
using std::endl;
// 函数定义
void func(int)
{
cout << "func1(int)" << endl;
}
void func(int*)
{
cout << "func2(int*)" << endl;
}
int main()
{
func(0);
func(NULL);
func(nullptr);
}
代码运行结果如下:
可以看到当函数重载既有 int 参数又有指针参数时,NULL 会优先使用 int 参数的函数。所以在 C++ 中,尽量使用 nullptr 作为空指针。
&esmp;首先,nullptr 是 C++ 的关键字,使用时不需要包含头文件。且在 C++11 中,sizeof(nullptr) 和 sizeof((void*)0) 的结果相同。