目录
一、历史
1.1 - C 语言的发展历程
1.2 - C++ 发展历程
二、C++ 关键字(C++98)
三、命名空间
3.1 - 命名空间的定义
3.2 - 命名空间的使用
四、C++ 输入&输出
五、缺省参数
5.1 - 缺省参数的概念
5.2 - 缺省参数的分类
六、函数重载
6.1 - 函数重载的概念
6.2 - C++ 是如何做到函数重载的
七、引用
7.1 - 引用的概念
7.2 - 引用作为函数参数
7.3 - 引用作为函数返回值
7.4 - 常引用
7.5 - 引用和指针
八、内联函数
8.1 - 为什么要使用内联函数
8.2 - 内联函数和宏
8.3 - 将内联函数定义在头文件中
8.4 - 慎用内联
九、auto 关键字(C++11)
9.1 - 进行类型推导
9.2 - 使用细则
9.3 - 适用场景
9.4 - 不适用场景
十、基于范围的 for 循环(C++11)
10.1 - 语法
10.2 - 使用条件
十一、指针空指 nullptr(C++11)
创作不易,可以点点赞,如果能关注一下博主就更好了~
参考资料:
C语言的历史 - 知乎 (zhihu.com)。
C++_百度百科 (baidu.com)。
详解c++的命名空间namespace - 知乎 (zhihu.com)。
C++ std命名空间 - 知乎 (zhihu.com)。
C++函数重载详解 (biancheng.net)。
C++引用精讲,C++ &用法全面剖析 (biancheng.net)。
C语言基础篇 (二十三) 运算中的临时匿名变量。
C++内联函数的使用 - 博客园 (cnblogs.com)。
【C++】内联函数为什么定义在头文件中?。
[C++11] auto关键字详解。
C语言丨一文带你了解auto关键字(又名隐形刺客) - 知乎 (zhihu.com)。
一、历史
1.1 - C 语言的发展历程
20 世纪 60 年代,AT&T 贝尔实验室的研究员 肯·汤普森(Ken Thompson) 发明了 B 语言,并使用 B 语言编写了一个名为 Space Travel 的游戏。
AT&T 是 American Telephone & Telegraph 的简称,即美国电话电报公司,由亚历山大·贝尔于 1877 年创立。
贝尔拥有电话的发明专利,但是有人指出,从意大利移民到美国的安东尼奥·梅乌奇(Antonio Meucci)才是电话的发明者。美国国会 2002 年 6 月 15 日 265 号决议确认安东尼奥·梅乌奇为电话的发明人。
他想玩这个游戏,所以背着老板找到了一台空闲的机器 PDP-7,但是由于这台机器没有操作系统,汤普森于是着手为 PDP-7 开发操作系统,后来这个 OS 被命名为 UNIX。
丹尼斯·里奇(Dennis Richie)也想玩这个游戏,所以加入了汤普森,合作开发 UNIX,他的主要工作是改进汤普森的 B 语言,最后产生了一个新的语言,即 C 语言。
C 语言源自 B 语言,而 B 语言源自 BCPL 语言,因此取 BCPL 的第二个字母,也就是 B 的下一个字母。
1973 年,肯·汤普森和丹尼斯·里奇用 C 语言重写了 UNIX。
肯·汤普森(左)与丹尼斯·里奇
1.2 - C++ 发展历程
20 世纪 70 年代中期,本贾尼·斯特劳斯特卢普(Bjarne Stroustrup) 在剑桥大学计算机中心工作。本贾尼希望开发一个既要编程简单、正确可靠,又要运行高效、可移植性的计算机程序设计语言,而以 C 语言为背景,以 Simula 思想为基础的语言,正好符合他的初衷和设想。
Simula 被认为是第一个面向对象的编程语言。
1979 年,本贾尼到了 AT&T 实验室,开始从事将 C 改良为带类的 C(C with classes)的工作。1983 年,该语言被正式命名为 C++(C Plus Plus)。
C++ 的历史版本:
阶段 | 内容 |
---|---|
C with classes | 类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等 |
C++1.0 | 添加虚函数概念,函数和运算符重载,引用、常量等 |
C++2.0 | 更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及 const 成员函数 |
C++3.0 | 进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理 |
C++98 | C++ 标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写 C+ +标准库,引入了 STL(Standard Template Library,标准模板库) |
C++03 | C++标准第二个版本,语言特性无大改变,主要:修订错误、减少多异性 |
C++05 | C++标准委员会发布了一份技术报告(Technical Report,TR1),正式更名 C++0x,即:计划在本世纪第一个 10 年的某个时间发布 |
C++11 | 增加了许多特性,使得 C++ 更像一种新语言,比如:正则表达式、基于范围 for 循环、auto 关键字、新容器、列表初始化、标准线程库等 |
C++14 | 对 C++11 的扩展,主要是修复 C++11 中漏洞以及改进,比如:泛型的 lambda 表达式, auto 的返回值类型推导,二进制字面常量等 |
C++17 | 在 C++11上 做了一些小幅改进,增加了 19 个新特性,比如:static_assert() 的文本信息可选,Fold 表达式用于可变的模板,if 和 switch 语句中的初始化器等 |
C++20 | 自 C++11 以来最大的发行版,引入了许多新的特性,比如:模块(Modules)、协程 (Coroutines)、范围(Ranges)、概念(Constraints)等重大特性,还有对已有特性的更 新:比如 Lambda 支持模板、范围 for 支持初始化等 |
C++23 | 制定 ing |
C++ 是基于 C 语言而产生的,C++ 既可以进行 C 语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。
二、C++ 关键字(C++98)
C 语言有 32 个关键字,而 C++ 总共有 63 个关键字。
asm auto
bool break
case catch char class const const_cast continue
delete do double dynamic_cast default
else enum explicit export extern
false float for friend
goto
if inline int long
mutable
namespace new
operator
private protected public
reinterpret_cast return register
short signed sizeof static static_cast struct switch
template this try typedef typeid typename throw true
union unsigned using
virtual void volatile
wchar_t while
三、命名空间
在 C++ 中,名称(name)可以是符号常量、变量、函数、结构、枚举、类和对象等等。工程越大,名称相互冲突的可能性就越大,另外使用多个厂商的类库时,也可能会导致名称冲突。为了避免在大规模程序的设计中,以及在程序员使用各种 C++ 库时,这些标识符的命名发生冲突,标准 C++ 引入关键字 namespace(命名空间),可以更好地控制标识符的作用域。
3.1 - 命名空间的定义
定义命名空间,需要使用关键字 namespace,后面跟命名空间的名字,然后再接一个 {} 即可,{} 中即为命名空间中的成员。
#include <stdio.h>
// 分别定义了名字为 A 和 B 的命名空间
// 一般开发中是使用项目名作为命名空间的名字
namespace A
{
int i = 0;
}
namespace B
{
int i = 1;
}
int i = 2;
int main()
{
int i = 3;
// 局部域 -> 全局域
printf("%d\n", i); // 3
// :: 是作用域限定符,前面是空白则表示全局域
printf("%d\n", ::i); // 2
printf("%d %d\n", A::i, B::i); // 0 1
}
// 注意:命名空间只能在全局范围内定义(下面为错误写法)
//int main()
//{
// namespace C
// {
// int i = 2;
// }
// return 0;
//}
命名空间可以嵌套:
#include <stdio.h>
namespace A
{
int i = 0;
namespace B
{
int i = 1;
}
}
int main()
{
printf("%d %d\n", A::i, A::B::i); // 0 1
return 0;
}
命名空间是开放的,可以随时把新的成员加入已有的命名空间中(常用):
#include <stdio.h>
namespace A
{
int i = 0;
}
namespace A
{
int j = 1;
}
int main()
{
printf("%d %d", A::i, A::j); // 0 1
return 0;
}
命名空间中的函数可以在命名空间之外定义:
#include <stdio.h>
namespace A
{
int i = 0;
int func(int x, int y);
}
// 成员函数在外部定义的时候,需要加作用域
int A::func(int x, int y)
{
printf("%d\n", i); // 访问命名空间的数据不用加作用域
return x + y;
}
int main()
{
printf("%d\n", A::func(10, 20)); // 30
return 0;
}
3.2 - 命名空间的使用
命名空间有以下三种使用方式:
-
命名空间名称 + 作用域限定符(::) + 成员名。
-
使用关键字 using 将命名空间中的某个成员引入。
#include <stdio.h> namespace A { int i = 0; } using A::i; int main() { printf("%d\n", i); // 0 return 0; }
-
使用 using namespcae 展开命名空间。
#include <stdio.h> namespace A { int i = 0; } using namespace A; int main() { printf("%d\n", i); // 0 return 0; }
直接展开命名空间会有风险,例如全局域中有一个同名的全局变量
i
,那么将命名空间 A 展开后,就会出现冲突(i
不明确)。因此不推荐在项目开发中直接展开命名空间,在日常练习中则可以那样操作。
四、C++ 输入&输出
#include <iostream>
using namespace std; // 在日常练习中可以使用,在项目开发中则不建议使用
int main()
{
int number = 0;
// << 是流插入运算符
// >> 是流提取运算符
cout << "Enter a decimal number:" << endl;
cin >> number;
cout << "The number you enterd is " << number << "." << endl;
return 0;
}
std 是 C++ 标准库的命名空间名。
早期标准库将所有功能定义在全局域中,声明在
.h
后缀的头文件中,使用时只需要包含对应头文件即可。后来标准 C++ 引入了命名空间的概念,并将标准库中的内容封装到了 std 命名空间中,同时为了不与原来的头文件混淆,规定标准 C++ 使用一套新的头文件,这套头文件的文件名不加.h
扩展名。并不是写了
#include <iostream>
就必须使用using namespace std;
,这样写的原因通常是为了把 std 命名空间中的内容暴露到全局域中(就像直接包含了iostream.h
这种没有命名空间的头文件一样),使标准 C++ 库用起来与传统iostream.h
一样方便。但不建议这样做,因为使用using namespace std;
的话就没有起到命名空间的作用,再次回到了如同没有涉及命名空间时,所有标识符都定义在全局作用域中的混乱情况,不利于程序员创建新对象。
五、缺省参数
5.1 - 缺省参数的概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
缺省的英文是 default,也就是默认的意思。
#include <iostream>
using namespace std;
void func(int n = 0)
{
cout << n << endl;
}
int main()
{
func(); // 0
func(10); // 10
return 0;
}
5.2 - 缺省参数的分类
-
全缺省参数:
#include <iostream> using namespace std; void func(int a = 10, int b = 20, int c = 30) { cout << a << ", " << b << ", " << c << endl; } int main() { func(); // 10, 20, 30 func(1); // 1, 20, 30 func(1, 2); // 1, 2, 30 func(1, 2, 3); // 1, 2, 3 return 0; }
-
半缺省参数:
#include <iostream> using namespace std; void func(int a, int b = 20, int c = 30) { cout << a << ", " << b << ", " << c << endl; } int main() { func(1); // 1, 20, 30 func(1, 2); // 1, 2, 30 func(1, 2, 3); // 1, 2, 3 return 0; }
注意:
-
半缺省参数必须从右往左依次给出,且不能间隔着给。
-
缺省参数不能在函数声明和定义中同时出现,否则编译器无法确定到底该用哪个缺省值。
-
Stack.h:
#pragma once // 顺序栈 typedef int SDataType; typedef struct Stack { SDataType* data; int top; int capacity; }Stack; // 基本操作 void StackInit(Stack* pst, int default_capacity);
Stack.cpp:
#include "Stack.h" #include <assert.h> #include <stdlib.h> void StackInit(Stack* pst, int default_capacity = 5) { assert(pst); pst->data = (SDataType*)malloc(sizeof(SDataType) * default_capacity); if (NULL == pst->data) { perror("initialization failed!"); exit(-1); } pst->top = 0; pst->capacity = default_capacity; }
test.cpp:
#include <Stack.h> int main() { Stack st; StackInit(&st, 4); // ok // StackInit(&st); // error --> 编译时出错,因为函数调用中的参数太少 // 所以这种情况下,缺省参数应该出现在函数声明中,而不是定义中 return 0; }
-
六、函数重载
6.1 - 函数重载的概念
函数重载(function overloading)指的是在同一个作用域内(同一个类、同一个命名空间等)有多个名称相同但参数列表不同的函数。函数重载的结果是让一个函数名拥有了多种用途,使得命名更加方便,调用更加灵活。
参数列表包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫作参数列表不同(注意:进仅仅是参数不同是不可以的)。
函数的返回值类型不能作为重载的依据。
#include <iostream>
using namespace std;
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void Swap(float* x, float* y)
{
float tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int i1 = 10, i2 = 20;
Swap(&i1, &i2);
cout << i1 << ", " << i2 << endl; // 20, 10
float f1 = 12.5, f2 = 22.5;
Swap(&f1, &f2);
cout << f1 << ", " << f2 << endl; // 22.5, 12.5
return 0;
}
函数重载和缺省参数:
#include <iostream>
using namespace std;
void test()
{
cout << "test()" << endl;
}
void test(int a = 0)
{
cout << "test(int a)" << endl;
}
int main()
{
test(10); // ok --> 即这两个同名的函数构成重载
// test(); // error --> 对重载函数的调用不明确
return 0;
}
6.2 - C++ 是如何做到函数重载的
在 C/C++ 中,一个程序要运行起来,需要经过编译和链接的过程。
其中编译又具体分为预编译(预处理)、编译、汇编这三个过程。
test.c:
#include <stdio.h> extern int Add(int x, int y); int main() { int a = 10; int b = 20; int sum = Add(a, b); printf("%d\n", sum); return 0; }
add.c:
int Add(int x, int y) { return x + y; }
当链接器看到
test.o
调用 Add,但是没有 Add 的地址,就会到add.o
的符号表中去找 Add 的地址,然后链接到一起。那么链接时,面对 Add 函数,链接器会使用哪个名字去找呢?每个编译器都有自己的函数名修饰规则,由于 Windows 下的 VS 的修饰规则过于复杂,而 Linux 下的 gcc 和 g++ 修饰规则简单易懂,因此下面分别使用 gcc 和 g++ 进行演示。
-
采用 C 语言编译器(gcc)编译后的结果:
可以看出 gcc 编译器并没有对函数名做修改。
-
采用 C++ 编译器(g++)编译后的结果:
可以看出 g++ 编译器的函数名修饰规则为:_Z + 函数名长度 + 函数名 + 参数类型首字母。
总结:由于 C 语言编译器在编译时并没对函数名做修改,无法区分同名函数,所以不支持函数重载;而 C++ 通过函数修改规则对同名函数进行了区分,只要参数列表不同,修饰出来的函数名就不同,所以支持函数重载。
七、引用
7.1 - 引用的概念
引用(Reference)是 C++ 相对于 C 语言的又一个扩充。引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。引用类似于 Windows 中的快捷方式,一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序;引用还类似于人的绰号、笔名等,使用绰号、笔名和本名都能表示一个人。
引用的特性:
-
引用在定义时必须初始化
-
引用一旦引用一个实体,则再不能引用其他实体
-
一个变量可以有多个引用
#include <iostream>
using namespace std;
int main()
{
// 一个变量可以有多个引用
int a = 10;
int& ra = a;
int& rra = ra;
cout << &a << " " << &ra << " " << &rra << endl; // 输出的三个地址相同
// 通过引用修改原始变量中所存储的数据
++ra;
++rra;
cout << a << " " << ra << " " << rra << endl; // 12 12 12
// 引用一旦引用一个实体,则再不能引用其他实体
int x = 20;
ra = x; // 将 x 的值赋给 ra
cout << a << " " << ra << " " << rra << endl; // 20 20 20
return 0;
}
引用在定义时需要添加
&
,在使用时不能添加&
,使用时添加&
表示取地址运算符或按位与运算符。
7.2 - 引用作为函数参数
在定义或声明函数时,可以将函数的参数指定为引用的形式,这样调用函数时就会将实参和形参绑定在一起,让它们都指代同一份数据。如此一来,如果在函数体中修改了形参的数据,那么实参的数据也会被修改,从而拥有 "在函数内部影响函数外部数据" 的效果。
#include <iostream>
using namespace std;
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
cout << a << " " << b << endl; // 20 10
return 0;
}
以值作为函数参数,形参是实参的一份临时拷贝;以引用作为函数参数则可以避免复制对象,提高效率,尤其是对于大对象的实参。
#include <iostream>
#include <time.h>
using namespace std;
#define N 1000
struct A
{
int arr[N];
};
void TestVal(A a) {}
void TestRef(A& a) {}
void TestValAndRefTime()
{
A a = { 0 };
// 以值作为函数参数
int begin1 = clock();
for (int i = 0; i < 1000000; ++i)
{
TestVal(a);
}
int end1 = clock();
// 以引用作为函数参数
int begin2 = clock();
for (int i = 0; i < 1000000; ++i)
{
TestRef(a);
}
int end2 = clock();
cout << "TestValTime: " << end1 - begin1 << endl; // 98
cout << "TestRefTime: " << end2 - begin2 << endl; // 24
}
int main()
{
TestValAndRefTime();
return 0;
}
7.3 - 引用作为函数返回值
#include <iostream>
using namespace std;
int& Count()
{
static int n = 0;
++n;
cout << &n << endl;
return n;
}
int main()
{
// 情形一
// int ret = Count();
// cout << &ret << endl; // 输出的两个地址不同
// cout << ret << endl; // 1
// 情形二
int& ret = Count(); // ret 此时是静态局部变量 n 的别名
cout << &ret << endl; // 输出的两个地址相同
cout << ret << endl; // 1
return 0;
}
情形一类似于:
int a = 10; int& ra = a; int b = ra;
情形二类型于:
int a = 10; int& ra = a; int& rra = ra;
在将引用作为函数返回值时应该注意,不要返回局部变量等临时变量的引用。
// 错误示例 int& Add(int x, int y) { int sum = x + y; return sum; }
以值作为函数的返回值,在返回期间,函数不会直接将变量本身返回,而是返回变量的一份临时拷贝;以引用作为函数返回值可以避免复制对象,提高效率,尤其是对于大对象的返回值。
#include <iostream>
#include <time.h>
using namespace std;
#define N 1000
struct A
{
int arr[N];
};
A a;
A TestVal() { return a; }
A& TestRef() { return a; }
void TestValAndRefTime()
{
// 以值作为函数返回值
int begin1 = clock();
for (int i = 0; i < 1000000; ++i)
{
TestVal();
}
int end1 = clock();
// 以引用作为函数返回值
int begin2 = clock();
for (int i = 0; i < 1000000; ++i)
{
TestRef();
}
int end2 = clock();
cout << "TestVal-Time: " << end1 - begin1 << endl; // 167
cout << "TestRef-Time: " << end2 - begin2 << endl; // 23
}
int main()
{
TestValAndRefTime();
return 0;
}
7.4 - 常引用
如果不希望通过引用来修改原始的数据,那么可以在定义时添加 const 限制,这种引用方式为常引用。
#include <iostream>
using namespace std;
int func()
{
static int x = 0;
cout << &x << endl;
return x;
}
int main()
{
int a = 10;
int& b = a; // ok(权限平移)
const int& c = a; // ok(权限缩小)
const int x = 20;
// int& y = x; // error(权限放大)
const int& y = x; // ok
// func 返回的是一个临时变量,临时变量具有常性
// int& ret = func(); // error
const int& ret = func(); // ok
cout << &ret << endl; // 输出的两个地址不同
return 0;
}
C语言基础篇 (二十三) 运算中的临时匿名变量。
7.5 - 引用和指针
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
// 指针
int a = 10;
int* pa = &a;
*pa = 20;
// 引用
int b = 20;
int& rb = b;
rb = 40;
return 0;
}
引用和指针的汇编代码对比:
引用和指针的不同点:
-
引用概念上定义一个变量的别名,指针存储一个变量的地址;
-
引用在定义时必须初始化,指针没有要求;
-
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
-
没有 NULL 引用,但有 NULL 指针;
-
在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占 4 个字节,64 位平台下占 8 个字节);
-
引用自加即引用的实体增加 1,指针自加即指针向后偏移一个类型的大小;
-
有多级指针,但是没有多级引用;
-
访问实体方式不同,指针需要显示解引用,引用编译器自己处理;
-
引用比指针使用起来相对更安全。
八、内联函数
8.1 - 为什么要使用内联函数
在 C++ 中我们通常定义以下函数来求两个整数的最大值:
int max(int x, int y)
{
return x > y ? x : y;
}
为这么一个小的操作定义一个函数的好处有:
-
阅读和理解 max 的调用,要比读一条等价的条件表达式并解释它的含义要容易得多;
-
如果需要做任何修改,修改函数要比找出并修改每一处等价表达式容易得多;
-
使用函数可以确保统一的行为,每个测试都保证以相同的方式实现;
-
函数可以重写,不必为其他应用程序重写代码。
虽然有这么多好处,但是写成函数有一个潜在的缺点,即调用函数比求解等价表达式要慢得多。在大多数的机器上,调用函数都要做很多工作,例如调用函数前要保存寄存器,并在返回时恢复,可能需要拷贝实参,程序转向新的位置执行等。
C++ 中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline 修饰的函数叫作内联函数,内联函数通常就是将它在程序中的每个调用点上 "内联地" 展开,例如:
inline int max(int x, int y)
{
return x > y ? x : y;
}
那么调用:cout << max(20, 10) << endl;
在编译时展开为:cout << 20 > 10 ? 20 : 10 << endl;
8.2 - 内联函数和宏
-
宏容易出错;
在 C 程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但使用起来像函数。编译预处理器用拷贝宏代码的方式取代函数调用,省去参数压栈、生成汇编语言的 CALL 调用、返回参数、执行 return 等过程,从而提高了速度。使用宏代码最大的缺点是容易出错,预处理器在拷贝宏代码时常常产生意想不到的边际效应。例如:
#define MAX(X, Y) (X) > (Y) ? (X) : (Y)
语句
int result = MAX(20, 10) + 30;
将被预处理器扩展为int result = (20) > (10) ? (20) : (10) + 30;
,由于算术运算符 "+" 的优先级比条件运算符 "?:" 高,所以 result 的结果并不等价于预期的 50,而是 20。如果把宏代码改写为:
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))
则可以解决由优先级引起的错误,但是即使使用修改后的宏代码也不是万无一失的,例如语句
int i = 20; int j = 10; int result = MAX(i++, j);
将被预处理器解释为int result = ((i++) > (j) ? (i++) : (j));
,那么 result 的结果为 21,i
被修改为 22。 -
宏不可调试;
宏的另一个缺点就是不可调试,但是内联函数是可以调试的。内联函数不是也像宏一样进行代码展开吗?怎么能够调试呢?其实内联函数的 "可调试" 不是说它展开后还能调试,而是在程序的调试(Debug)版本里它根本就没有真正内联,编译器像普通函数那样为它生成含有调试信息的可执行代码。在程序的发行(Release)版本里,编译器才会实施真正的内联。有的编译器还可以设置函数内联开关,例如 Visual C++。
8.3 - 将内联函数定义在头文件中
-
对于普通函数,声明一般放在头文件中,定义则放在 .cpp 文件中。
Max.h:
#pragma once int max(int x, int y);
Max.cpp:
#include "Max.h" int max(int x, int y) { return x > y ? x : y; }
test.cpp:
#include "Max.h" #include <iostream> using namespace std; int main() { cout << max(20, 10) << endl; // 20 return 0; }
在编译阶段找到函数的声明,在链接阶段去找函数的定义。
-
对于内联函数,应该将定义放在头文件中。
Max.h:
inline int max(int x, int y) { return x > y ? x : y; }
test.cpp:
#include "Max.h" #include <iostream> using namespace std; int main() { cout << max(20, 10) << endl; // 20 return 0; }
在编译期间编译器需要找到内联函数的定义,然后在调用点进行展开。
8.4 - 慎用内联
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
内联说明只是向编译器出发的一个请求,编译器可以选择忽略这个请求。一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个 75 行的函数也不大可能在调用点内联地展开。
九、auto 关键字(C++11)
9.1 - 进行类型推导
在 C 语言和 C++98 标准中,auto 关键字用于修饰变量(自动存储的局部变量)。
存储类是 C 语言和 C++ 语言的标准中,变量与函数的可访问性(即作用域范围 scope)与生命周期(life time)。
存储类可分为 auto、register、static、extern、mutable、thread_local 等。
auto 存储类是所有局部变量默认的存储类。
在 C++11 中,标准委员会赋予了 auto 全新的含义,不再用于修饰变量,而是作为一个类型指示符,指示编译器在编译时推导 auto 声明的变量的数据类型。
在 Linux 平台下,编译需要加 -std=c++11
参数。
#include <iostream>
using namespace std;
int func()
{
return 20;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = 3.14;
auto e = func();
cout << b << " " << c << " " << d << " " << e << endl; // 10 a 3.14 20
cout << typeid(b).name() << endl; // int
cout << typeid(c).name() << endl; // char
cout << typeid(d).name() << endl; // double
cout << typeid(e).name() << endl; // int
return 0;
}
使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型。因此 auto 并非是一种 "类型" 的声明,而是一个类型声明时的 "占位符",编译器在编译期会将 auto 替换成变量实际的类型。
9.2 - 使用细则
-
auto 与指针和引用结合起来使用:
// 用 auto 声明指针类型时,auto 和 auto* 没有任何区别, // 但用 auto 声明引用类型时则必须加 & #include <iostream> using namespace std; int main() { int a = 10; auto p1 = &a; auto* p2 = &a; auto& r = a; cout << typeid(p1).name() << endl; // int* cout << typeid(p2).name() << endl; // int* cout << typeid(r).name() << endl; // int (*p1)++; (*p2)++; r++; cout << a << " " << *p1 << " " << *p2 << " " << r << endl; // 13 13 13 13 return 0; }
-
在同一行定义多个变量:
// 当在同一个定义多个变量时,这些变量必须是相同的类型,否则编译器会报错, // 因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量 int main() { auto a = 1, b = 2; // ok // auto c = 3, d = 4.0; // error return 0; }
9.3 - 适用场景
不要滥用 auto,该关键字在编程时真正的用途如下:
-
代替冗长复杂的类型。
-
模板函数中声明依赖模板参数的变量类型。
-
模板函数返回值依赖模板参数的变量类型。
-
用于 lambda 表达式中。
9.4 - 不适用场景
-
auto 不能作为函数的参数:
int Add(auto x, auto y) { return x + y; }
以上的代码会编译失败,auto 不能作为形参类型,因为编译器无法对 x 和 y 的实际类型进行推导。
-
auto 不能直接用来声明数组:
int arr[] = { 1, 2, 3 }; // ok auto num[] = { 4, 5, 6 }; // error
十、基于范围的 for 循环(C++11)
10.1 - 语法
在 C++ 98 中如果要遍历一个数组,可以按照以下方式进行:
#include <iostream>
using namespace std;
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
int n = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < n; ++i)
{
arr[i] *= 2;
}
for (int* p = arr; p < arr + n; ++p)
{
cout << *p << " ";
}
// 2 4 6 8 10
cout << endl;
return 0;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还容易犯错误,因此 C++11 中引入了基于范围的 for 循环。for 循环后的括号由冒号 :
分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
#include <iostream>
using namespace std;
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
for (auto& e : arr)
{
e *= 2;
}
for (auto e : arr)
{
cout << e << " ";
}
// 2 4 6 8 10
cout << endl;
return 0;
}
与普通循环类型,可以用 continue 来跳过本次循环,也可以用 break 来结束整个循环。
10.2 - 使用条件
-
for 循环迭代的范围必须是确定的。对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin 和 end 的方法,begin 和 end 就是 for 循环迭代的范围。
void PrintArr(int arr[]) { for (auto e : arr) { cout << e << " "; } cout << endl; }
以上代码有问题,因为范围不确定。
-
迭代的对象要实现 ++ 和 == 的操作。
十一、指针空指 nullptr(C++11)
良好的 C/C++ 编程习惯应该是在定义一个变量时给变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
int* p1 = NULL;
NULL 实际上是一个宏,在头文件中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
从中可知,在 C 中 NULL 是 ((void *)0)
指针,在 C++ 中 NULL 则是字面常量 0。因此在使用空值的指针时,不可避免地会遇到一些麻烦,例如:
#include <iostream>
using namespace std;
void test(int)
{
cout << "test(int)" << endl;
}
void test(int*)
{
cout << "test(int*)" << endl;
}
int main()
{
test(NULL); // test(int)
return 0;
}
在 C++ 中,如果不使用形参,则可以省略形参名。
上面程序的本意是想通过
test(NULL)
调用指针版本的test(int*)
函数,但是由于 NULL 被定义为 0,因此与程序的初衷相悖。在 C++ 98 中,字面常量 0 既可以是一个整型数字,也可以是无类型的指针常量,但是编译器默认情况下将其看成一个整型常量,如果要将其按照指针方式来使用,必须对其进行强制,即
(void *)0
。注意:
使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++ 11 作为新关键字引入的。
在 C++ 11 中,
sizeof(nullptr)
与sizeof((void *)0)
所占的字节数相同。为了提高代码的健壮性,在后续表示指针空值时最好使用 nullptr。