C++ 总计 63 个关键字,C 语言 32 个关键字。C 语言的关键字在 C++ 中继续可以使用。 C++ 兼容 C 的绝大多数语法。
在 C / C++ 中,变量、函数和类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是 对标识符的名称进行本地化,以避免命名冲突或名字污染 ,namespace 关键字的出现就是针对这种问题的。
#include
#include
int rand = 10;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
int main()
{
printf("%d\n", rand);
return 0;
}
// 编译后报错:error C2365: “rand”: 重定义;以前的定义是“函数”
在不同的作用域中,可以定义同名的变量;但在同一作用域下,不能定义同名的变量。
用命名空间来解决变量 rand 和 stdlib 库里的命名冲突。
#include
#include
namespace yln // 定义了一个命名空间域
{
int rand = 10; // 定义变量
int Add(int x, int y) // 定义函数
{
return x + y;
}
struct Node // 定义结构体类型
{
struct Node* next;
int val;
};
}
int main()
{
printf("%p\n", rand);//函数指针
printf("%d\n", yln::rand); // rand变量;'::'叫做域作用限定符
yln::Add(3, 5);//调用函数
struct yln::Node node1; // 结构体
return 0;
}
定义命名空间,需要使用到 namespace 关键字,后面 + 命名空间的名字 ,然后 接一对 {} 即可,{} 中即为命名空间的 成员 。
namespace yln
{
int rand = 10; // 命名空间中可以定义变量
int Add(int left, int right) // 命名空间中可以定义函数
{
return left + right;
}
struct Node // 命名空间中可以定义类型
{
int val;
struct Node* next;
};
}
yln 是命名空间的名字,一般开发中是用项目名字做命名空间名。
// test.cpp
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
int main()
{
printf("%d\n", N1::N2::Sub(10, 20)); // 访问嵌套命名空间
return 0;
}
// test.h
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
同一个工程中的 test.h 和上面 test.cpp 中的两个 N1 会被编译器合并成在同一个命名空间中。
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
int main()
{
printf("%d\n", N::a);
return 0;
}
⚪优点:不存在命名污染。
⚪缺点:如果要去访问多个命名空间里的东西时,需要一一指定。
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
⚪优点:不会造成大面积的污染;把常用的展开后,也不需要一一指定。
using namespace N;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
Add(10, 20);
return 0;
}
⚪优点:方便。
⚪缺点:自己定义的东西会暴露出去,导致命名污染。
#include
using namespace std;
int main()
{
cout << "Hello world!" << endl;
return 0;
}
std 是 C++ 标准库的命名空间名,C++ 将标准库的定义实现都放到这个命名空间中。
- 使用 cout 标准输出对象(控制台)和 cin 标准输入对象(键盘)时,必须包含
头文件 以及按命名空间使用方法使用 std。- cout 和 cin 类似 C 语言的 printf 和 scanf,这里先简单了解一下,因为对于 C 语言中的 I/O 是函数,而 C++ 是对象。cout 和 cin 是全局的流对象,endl 是特殊的 C++ 符号,表示换行输出,他们都包含在包含 <iostream> 头文件中。
- << 是流插入运算符,>> 是流提取运算符。
- 使用 C++ 输入输出更方便,不需要像 printf / scanf 输入输出时那样,需要手动控制格式。
- C++ 的输入输出可以自动识别变量类型。
- 实际上 cout 和 cin 分别是 ostream 和 istream 类型的对象,>> 和 << 也涉及运算符重载等知识。
注意 :早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在 std 命名空间下,为了和 C 头文件区分,也为了正确使用命名空间,规定 C++ 头文件不带 .h ;旧编译器 (vc 6.0) 中还支持格式,后续编译器已不再支持,所以推荐使用 + std 的方式。
#include
using namespace std;
int main()
{
int a;
double b;
char c;
// 可以自动识别变量的类型
cin >> a;
cin >> b >> c;
cout << a << endl;
cout << b << " " << c << endl;
return 0;
}
关于 cout 和 cin 还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等。但是因为 C++ 是兼容 C 语言的用法,这些又用得不是很多,所以我们一般可以直接用按照 C 语言的用法来写,就不用再进一步学习 C++ 这方面的内容了。
std 命名空间的使用惯例:std 是 C++ 标准库的命名空间,如何展开 std 使用更合理呢?
- 在日常练习中,建议直接 using namespace std 即可,这样就很方便。
- using namespace std 展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型 / 对象 / 函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现这种情况。所以建议在项目开发中使用,像 std::cout 这样使用时指定命名空间 + using std::cout 展开常用的库对象 / 类型等方式。
#include
// 展开常用即可,工程项目中常见的对命名空间的用法
using std::cout;
using std::endl;
int main()
{
// 只要是库里的都得指定std
std::cout << "Hello world!" << std::endl
//cout << "Hello world!" << endl;
return 0;
}
缺省参数 是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值 - 0
Func(10); // 传参时,使用指定的实参 - 10
return 0;
}
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
void Func(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
注意:
- 半缺省参数必须从右往左依次给出,不能间隔着给。
- 缺省参数不能在函数声明和定义中同时出现。
缺省值必须是 常量 或者 全局变量 。 C 语言不支持(编译器不支持)。
// test.h
void Func(int a = 10);
// test.cpp
void Func(int a = 20)
{}
注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不相同,那么编译器就无法确定到底该用那个缺省值。
函数重载 :是函数的一种特殊情况,C++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 / 类型 / 类型顺序)不同,常用来处理实现 功能类似数据类型不同 的问题。
// test1.cpp
#include
using namespace std;
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// test2.cpp
void f(int a, char b)
{
cout << "f(int a, char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
在 C / C++ 中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
可参考【C语言】程序环境预处理 -- 详解_炫酷的伊莉娜的博客-CSDN博客 ,内有详细介绍。
实际项目通常是由多个头文件和多个源文件构成,而通过 C 语言阶段学习的编译链接,我们 知道,当前 test2.cpp 中调用了 test1.cpp 中定义的 Add 函数,在编译后链接前,test2.o 的目标文件中没有 Add 的函数地址,因为 Add 是在 test1.cpp 中定义的,所以 Add 的地址在 test1.o 中,那该怎么办呢?
所以链接阶段就是专门处理这种问题,链接器看到 test2.o 调用 Add,但是没有 Add 的地址,就会到 test1.o 的符号表中找 Add 的地址,然后链接到一起。
那么在链接时,面对 Add 函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。由于 Windows 下 VS 的修饰规则过于复杂,而 Linux 下 G++ 的修饰规则简单易懂,下面我们使用了 G++ 演示了这个修饰后的名字。
结论 :在 Linux 下,采用 GCC 编译完成后,函数名字的修饰没有发生改变,函数名相同时,无法区分函数。
结论 : 在 Linux 下,采用 G++ 编译完成后,函数名字的修饰发生改变,编译器将函数参 数类型信息添加到修改后的名字中。
补充:通过上面我们可以看出 GCC 的函数修饰后名字不变,而 G++ 的函数修饰后变成 <_Z + 函数长度 + 函数名 + 类型首字母>。
以 Add 函数为例:
_Z 是 GCC 编译器的修饰前缀,表示这是一个 C++ 函数名。
3 是函数名的长度。
Add 是函数名。
ii / dd 是函数参数类型的首字母,如果是 int* i,那么就是 Pi。
通过这里就理解了 C 语言没办法支持重载,因为同名函数没办法区分。而 C++ 是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。如果两个函数的函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
有时候在 C++ 工程中可能需要将某些 (部分) 函数按照 C 的风格来编译,在函数前加 extern “C”,意思是告诉编译器,将该函数按照 C 语言规则来编译。比如:tcmalloc 是 google 用 C++ 实现的一个项目,他提供 tcmallc() 和 tcfree两个接口来使用,但如果是 C 项目就没办法使用,那么他就使用 extern “C” 来解决。
extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}
- C++ 项目可以调用 C++ 库,也可以调用 C 的库,C++ 是直接兼容 C 的。
- C 项目可以调用 C 库,也可以使用 extern "C" 调用 C++ 库(C++ 提供的函数加上 extern "C")。
引用 不是新定义一个变量,而是给已 存在变量 取了一个 别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体;
void TestRef()
{
int a = 10;
int& ra = a;
printf("%p\n", &a);
printf("%p\n", &ra);
// 地址相同
}
注意:引用类型必须和引用实体是同种类型的。
- 引用在定义时必须初始化。
- 一个变量可以有多个引用。
- 引用一旦引用一个实体,就再不能引用其他的实体。
void TestRef()
{
int a = 10;
// int& ra; // 编译出错 - 没有对引用变量进行初始化
// 可以有多个引用
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra);
}
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 编译出错,a为常量
const int& ra = a;
//int& b = 10; // 编译出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 编译出错,类型不同
const int& rd = d;
}
在 C++ 中,引用必须与其引用的对象具有相同的类型,或者可以通过隐式类型转换来匹配。
- 函数传参如果想减少拷贝使用引用传参,如果函数中不改变这个参数最好使用 const 引用传参。
- const 引用的好处是保护实参,避免被误改,且它可以传普通对象也可以传 const 对象。
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
int& Add(int a, int b)
{
int c = a + b;
return c; // 函数结束后,c变量就没有意义了
}
int main()
{
int& ret = Add(1, 2); // 引用Add函数返回值
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl; // 7
// 具体得看平台销毁栈帧时是否会清理栈帧空间,但是这种写法本身就是越界的,是错误的
return 0;
}
- 函数运行时,系统需要给该函数开辟独立的栈空间,用来保存该函数的形参,局部变量以及一些寄存器信息等。
- 函数运行结束后,该函数对应的栈空间就被系统回收了。
- 空间被回收指该块栈空间暂时不能使用,但是内存本身还在。
注意 :如果函数返回时,出了函数的作用域,如果返回对象还在(还没还给系统),则可以使用引用返回;如果已经还给系统了,则必须使用传值返回。
#include
struct A{
int a[10000];
};
void TestFunc1(A a)
{}
void TestFunc2(A& a)
{}
void TestRefAndValue()
{
A a;
// 1、以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
{
TestFunc1(a);
}
size_t end1 = clock();
// 2、以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
{
TestFunc2(a);
}
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间:
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include
struct A{
int a[10000];
};
A a;
A TestFunc1() // 值返回
{
return a;
}
A& TestFunc2() // 引用返回
{
return a;
}
void TestReturnByRefOrValue()
{
// 1、以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
{
TestFunc1();
}
size_t end1 = clock();
// 2、以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
{
TestFunc2();
}
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
通过上述代码运行后的结果比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main()
{
int a = 10;
int& ra = a;
cout << "&a = " << &a << endl;
cout << "&ra = " << &ra << endl;
// 地址相同,是取同一块空间
return 0;
}
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
【引用和指针的不同点】
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
没有 NULL 引用,但有 NULL 指针。 在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64 位平台下占 8 个字节 )。 引用自加即引用的实体增加 1,指针自加即指针向后偏移一个类型的大小。 有多级指针,但是没有多级引用。 访问实体方式不同,指针需要显式解引用,引用是编译器自己处理。引用比指针使用起来相对更安全,指针容易出现野指针、空指针等非法访问问题。
以 inline 修饰的函数叫做 内联函数 ,编译时 C++ 编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
如果在上述函数前增加 inline 关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
- inline 是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline 对于编译器而言只是一个建议,不同编译器关于 inline 实现机制可能不同,一般建 议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用 inline 修饰,否则编译器会忽略 inline 特性。下图为 《C++prime》 第五版关于 inline 的建议:
- inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。
// test.h
#include
using namespace std;
inline void f(int i);
// test.cpp
#include "test.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "test.h"
int main()
{
f(10);
return 0;
}
链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用。
优点:
- 增强代码的复用性。
- 提高性能。
缺点:
- 不方便调试宏。(因为预编译阶段进行了替换)
- 导致代码可读性差,可维护性差,容易误用。
- 没有类型安全的检查 。
- 常量定义换用 const enum 。
- 短小函数定义换用内联函数。
由于接触到的程序愈变复杂,导致类型难于拼写 ,含义不明确导致容易出错。
#include
#include
std::map
#include
#include
使用 typedef 给类型取别名确实可以简化代码,但是 typedef 有会遇到新的难题:
typedef char* pstring;
int main()
{
const pstring p1;
const pstring* p2;
return 0;
}
编译结果失败。在上述代码中,pstring 被定义为指向 char 类型的指针,而 const pstring p1; 则表示 p1 是一个常量指针,指向的值不能更改。然而,在定义常量指针时,必须给它进行初始化,而这里没有对 p1 进行初始化,因此会导致编译失败。同样地,const pstring* p2; 表示 p2 是一个指向常量指针的指针,同样需要进行初始化。编译器会报错提示缺少初始化。
在早期 C / C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,为什么呢?因为在 C / C++ 中,如果没有使用任何存储类别关键字(如 auto、static、extern 等),则变量默认为自动存储类别。因此,使用 auto 关键字对于自动存储类别的变量来说没有实际意义。
C++11 中,标准委员会赋予了 auto 全新的含义即:auto 不再是一个存储类型指示符,而是作为一 个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl; // int
cout << typeid(c).name() << endl; // char
cout << typeid(d).name() << endl; // int
// auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
注意 : 使用 auto 定义变量时必须对其进行初始化 ,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型。因此 auto 并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在 编译 期会将 auto 替换为变量实际的类型。
用 auto 声明指针类型时, 用 auto 和 auto* 没有任何区别 ,但用 auto 声明引用类型 时则必须 加 & 。
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl; // int*
cout << typeid(b).name() << endl; // int*
cout << typeid(c).name() << endl; // int
*a = 20;
*b = 30;
c = 40;
return 0;
}
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错。因为 编译器实际只对第一个类型进行推导 ,然后用推导出来的类型来定义其他的变量。
void TestAuto()
{
auto a = 1, b = 2; // 编译成功
auto c = 3, d = 4.0; // 编译失败,因为c和d的初始化表达式类型不同
}
void TestAuto(auto a)
{}
// 编译失败:auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto()
{
int a[] = {1, 2, 3};
auto b[] = {4,5,6}; // error
}
在 C++98 中如果要遍历一个数组,可以按照以下方式进行:
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;
}
}
void TestFor()
{
int array[] = {1, 2, 3, 4, 5};
for(auto& e : array)
{
e *= 2;
}
for(auto e : array)
{
cout << e << " ";
}
return 0;
}
注意:与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环。
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin 和 end 的方法,begin 和 end 就是 for 循环迭代的范围。
void TestFor(int array[])
{
for(auto& e : array) // 该代码有问题,因为for的范围不确定
{
cout << e << endl;
}
}
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
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(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0,因此与程序的初衷相悖。在 C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void*)0 。
注意:
- 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11 作为新关键字引入的。
- 在 C++11 中,sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr。