目录
[2.1 命名空间定义](#2.1 命名空间定义)
[2.2 命名空间的使用](#2.2 命名空间的使用)
[4.1 缺省参数的概念](#4.1 缺省参数的概念)
[4.2 缺省参数分类](#4.2 缺省参数分类)
[4.3 缺省参数的应用](#4.3 缺省参数的应用)
[5.1 函数重载的概念](#5.1 函数重载的概念)
[5.2 C++支持函数重载的原理--函数名修饰](#5.2 C++支持函数重载的原理--函数名修饰)
[6.1 引用的概念](#6.1 引用的概念)
[6.2 引用的特性](#6.2 引用的特性)
[6.3 引用的使用场景](#6.3 引用的使用场景)
[6.4 传值和传引用效率对比](#6.4 传值和传引用效率对比)
[6.5 常引用](#6.5 常引用)
[6.6 引用和指针的区别](#6.6 引用和指针的区别)
[7.1 概念](#7.1 概念)
[7.2 特性](#7.2 特性)
[8.1 auto的功能](#8.1 auto的功能)
[8.2 auto不能推导的场景](#8.2 auto不能推导的场景)
[9.1 范围for的语法](#9.1 范围for的语法)
[9.2 范围for的使用条件和注意事项](#9.2 范围for的使用条件和注意事项)
一、C++关键字(C++98)
C++总计63个关键字,C语言总结32个关键字。
|----------------|------------------|----------------------|-----------------|--------------|--------------|
| asm | do | if | return | try | continue |
| auto | double | inline | short | typedef | for |
| bool | dynamic_cast | int | signed | typeid | public |
| break | else | long | sizeof | typename | throw |
| case | enum | mutable | static | union | wchar_t |
| catch | explicit | namespace | static_cast | unsigned | default |
| char | export | new | struct | using | friend |
| class | extern | operator | switch | virtual | register |
| const | false | private | template | void | true |
| const_cast | float | protected | this | volatile | while |
| delete | goto | reinterpret_cast | | | |
二、命名空间
在C/C++中,变量、函数以及后面的类都是大量存在的。这些变量、函数和类的名字都是存在全局作用域中,可能会导致很多命名冲突。使用命名空间的目的是对标识符进行本地化,以避免命名冲突和名字污染。namespace关键字的出现就是针对这种问题的。
cpp
#include<stdlib.h>
int rand = 10;
int main()
{
//error C2365: "rand": 重定义;以前的定义是"函数"
printf("%d\n", rand);
return 0;
}
2.1 命名空间定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}内部的成员便是命名空间成员。
cpp
//命名空间的定义
namespace A1
{
//命名空间内可以定义变量、函数以及结构
int rand = 10;
int Add(int x, int y)
{
return x + y;
}
struct Node
{
int data;
struct Node* next;
};
}
//命名空间也可以嵌套定义
namespace B1
{
int b1 = 1;
//B1里面嵌套定义B2
namespace B2
{
int b2 = 2;
}
}
注:一个命名空间就是一个作用域,命名空间中所有内容都局限于这个作用域当中。多个名字相同的命名空间会自动合并为一个命名空间。
2.2 命名空间的使用
2.2.1 加命名空间的名称和作用域限定符(::)对命名空间内成员的访问:
cpp
namespace A2
{
//变量
int rand = 10;
//函数
int Add(int x, int y)
{
return x + y;
}
//结构
struct Node
{
int data;
struct Node* next;
};
}
int mian()
{
//对变量的访问
A2::rand;
//对函数的访问
A2::Add(1, 2);
//对结构的访问
struct A2::Node n1;
return 0;
}
2.2.2 全部展开命名空间:
如果需要不用指定就想访问命名空间内部的变量、函数或结构的话,可以采用全部展开命名空间的方式。
cpp
namespace A2
{
//变量
int a2 = 10;
//函数
int Add(int x, int y)
{
return x + y;
}
//结构
struct Node
{
int data;
struct Node* next;
};
}
using namespace A2;
//展开后的访问
//对变量的访问
a2;
//对函数的访问
Add(1, 2);
//对结构的访问
struct Node n1;
注:全部展开命名空间会有很大的风险,比如造成命名冲突的问题。
2.2.3 部分展开命名空间:
当希望单独展开某一变量、函数或结构时,可以采用部分展开命名空间的方式。
cpp
namespace A2
{
//变量
int a2 = 10;
//函数
int Add(int x, int y)
{
return x + y;
}
//结构
struct Node
{
int data;
struct Node* next;
};
}
//部分展开命名空间
//单独展开某一变量
using A2::a2;
//单独展开某一函数
using A2::Add;
//单独展开某一结构
using A2::Node;
2.2.4 using namespace std:
cpp
//全部展开std命名空间,std是C++标准库的命名空间
using namespace std;
注:std是C++标准库的命名空间,日常练习中可以全部展开,但在项目中全部展开可能会导致命名冲突的问题,更推荐部分展开std命名空间。
2.2.5 头文件的展开和命名空间的展开之间的区别:
头文件的展开是将头文件的内容拷贝过来。
命名空间的展开是对命名空间内容的访问授权。
三、C++输入和输出
1.使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含<iostream>头文件,并且需要展开命名空间std。
2.cout和cin是全局的流对象,endl是特殊的C++符号,代表换行输出,他们都包含在<iostream>头文件中。
3.<<是流插入运算符,>>是流提取运算符。
4.使用C++输入输出更方便,不需要像printf/scanf输入输出那样,需要手动控制格式。并且C++的输入输出能自动识别变量类型。
5.实际上cout和cin是ostream和istream类型的对象,>>和<<涉及运算符重载的知识。
cin和cout的使用:
cpp
#include<iostream>
using namespace std;
int i;
double k;
cin >> i >> k;
cout << i << ' ' << k << endl;
注:cin和cout能自动识别类型的原理是函数重载。
四、缺省参数
4.1 缺省参数的概念
缺省参数是指在定义或声明一个函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,如果指定了实参,则直接用实参。
cpp
//缺省参数
void Func(int x = 1)
{
cout << x << endl;
}
int main()
{
//没有指定实参则会使用形参的缺省参数
Func();
//有指定的实参则直接使用实参进行传参
Func(3);
return 0;
}
4.2 缺省参数分类
全缺省参数:
cpp
//全缺省参数
void Func(int a = 10, int b = 20, int c = 30)
{
cout << a << ' ' << b << ' ' << c << endl;
}
int main()
{
//调用方式
Func();
Func(1);
Func(1, 2);
Func(1, 2, 3);
return 0;
}
半缺省参数:
cpp
//半缺省参数
void Func(int a, int b, int c = 30, int d = 40)
{
cout << a << ' ' << b << ' ' << c << ' ' << d << endl;
}
int main()
{
//调用方式
Func(1, 2);
Func(1, 2, 3);
Func(1, 2, 3, 4);
return 0;
}
注:对于带有缺省参数的函数,显示传参时必须从左往右依次显示传参。对于半缺省参数,缺省参数必须从右往左缺省。
4.3 缺省参数的应用
cpp
#Stack.h
typedef struct Stack
{
int* a;
int top;
int capacity;
}ST;
//声明
void STInit(ST* pst, int N = 4);
#Stack.cpp
#include "Stack.h"
//定义
void STInit(ST* pst, int N)
{
pst->a = (int*)malloc(sizeof(int) * N);
pst->top = 0;
pst->capacity = N;
}
#main.cpp
#include "Stack.h"
int main()
{
//对于不清楚要要插入的数据个数可以使用缺省参数设置的默认大小
ST st;
STInit(&st);
//对于清楚要插入的数据个数可以直接在初始化直接传入个数,降低频繁的扩容消耗
ST st1;
STInit(&st, 10);
return 0;
}
注:缺省参数不能在函数的声明和定义中同时存在,只需要在声明中给缺省参数就行。
五、函数重载
5.1 函数重载的概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能相似的同名函数,但需要这些同名函数的形参列表(参数个数或参数类型或参数类型的顺序)不同,常用来处理功能相似但数据类型不同的问题。
构成函数重载的情况:
cpp
//构成函数重载的情况
void Func(int a, char b)
{
cout << a << ' ' << b << endl;
}
//参数类型不同
void Func(char a, char b)
{
cout << a << ' ' << b << endl;
}
//参数的个数不同
void Func(int a, char b, double c)
{
cout << a << ' ' << b << ' ' << c << endl;
}
//参数类型的顺序不同
void Func(char a, int b)
{
cout << a << ' ' << b << endl;
}
不构成函数重载的情况:
cpp
void Func(int a, char b)
{
cout << a << ' ' << b << endl;
}
//只有形参顺序不同不能构成重载
//error C2084: 函数"void Func(int,char)"已有主体
void Func(int b, char a)
{
cout << a << ' ' << b << endl;
}
//只有返回值不同也不能构成重载
//error C2556: "int Func(int,char)": 重载函数与"void Func(int,char)"只是在返回类型上不同
int Func(int a, char b)
{
cout << a << ' ' << b << endl;
}
namespace A1
{
void Func(int a, char b)
{
cout << a << ' ' << b << endl;
}
}
//在不同作用域内,同名函数也不会构成函数重载
namespace A2
{
void Func(char a, int b)
{
cout << a << ' ' << b << endl;
}
}
特殊情况:
cpp
void Func(int a)
{
cout << a << endl;
}
//构成函数重载,但在调用过程中可能会存在歧义
//error C2668: "Func": 对重载函数的调用不明确
void Func(int a, int b=10)
{
cout << a << endl;
}
int main()
{
Func(1);
return 0;
}
5.2 C++支持函数重载的原理--函数名修饰
在C/C++的程序运行期间,要经历以下几个阶段:预处理、编译、汇编、链接。

预处理:Test.cpp
头文件的展开、宏替换、去除注释、条件编译(防止头文件重复包含)
编译:Test.i
检查语法、生成汇编代码
汇编:Test.s
将汇编代码转化为二进制的机器码
链接:Test.o
合并链接、生成可执行程序

1.实际项目通常是由多个头文件和多个源文件构成,每个源文件里都会生成对应的符号表,而链接的作用就是将互相调用的文件中的符号表进行合并链接在一起。
2.不同的编译器都会有不同的修饰规则,gcc的函数修饰后名字不变,而g++的函数修饰后变成_Z+函数长度+函数名+类型首字母。
采用C语言编译器后的结果:

采用C++编译器的结果:

3.Windows下的名字修饰规则:


4.通过不同编译器的修饰规则就可以了解到C语言没办法支持函数重载,因为修饰后的函数名依旧一样,没有办法进行有效的区分。而C++通过函数修饰后只要参数不同,函数的修饰后的结果也会不同,能够进行有效的区分,因此C++就能够支撑函数重载。
5.函数名修饰规则不带入返回值。即便函数名修饰规则带入了返回值也不能构成函数重载,因为无法确定调用时要具体调用哪种函数。
六、引用
6.1 引用的概念
引用不是新定义一个变量,而是给已经存在的变量取了一个别名,编译器不会为引用变量开辟一块新的空间,它和引用的变量共用同一块空间。

注:类型+&+引用变量名(对象名)=引用实体。这里的&是一个符号多重意思,当&处于类型和变量名之间时代表引用,当&前面没有类型时代表取地址。
6.2 引用的特性
1.引用在定义时必须要进行初始化。
2.一个变量可以有多个引用。
3.引用一旦引用一个实体,就不能继续去引用其他的实体。
cpp
int a = 0;
//int& ra; //这句语句会报错
//这里的a、b、c、d使用的都是同一块空间
int& b = a;
int& c = a;
int& d = b; //可以引用引用变量
注:C++的引用不能改变指向,但Java的引用可以改变指向。
6.3 引用的使用场景
1.引用做参数:
引用可以做输出型参数,当实参采用引用的方式传递给形参时,此时形参的改变能够影响实参。
cpp
void swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int a = 1;
int b = 2;
swap(a, b);
cout << a << endl;
cout << b << endl;
2.引用做返回值:
引用可以做返回值,可以在函数外对返回的变量进行修改和操作。
cpp
int& func()
{
static int a = 0;
a++;
return a;
}
注:如果出了函数作用域后返回的对象还在,就可以用引用返回,比如静态对象、全局对象、堆上动态申请的对象等。如果返回的对象是函数内创建的临时对象,出了函数作用域后就被销毁了,那么就不能使用引用作为返回值,此时可能会出现随机值的后果。
例如:用引用作为返回值,返回临时对象,且用引用对象进行接收

6.4 传值和传引用效率对比
6.4.1 传值传参和传引用传参的消耗对比:
cpp
struct S
{
int a[100000];
};
//传值
void func1(struct S s)
{}
//传引用
void func2(struct S& s)
{}
int main()
{
S s;
int i = 0;
int begin1 = clock();
for (i = 0; i < 10000; i++)
{
func1(s);
}
int end1 = clock();
printf("10000次传值传参消耗的时间:%d\n", end1 - begin1);
int begin2 = clock();
for (i = 0; i < 10000; i++)
{
func2(s);
}
int end2 = clock();
printf("10000次传引用传参消耗的时间:%d\n", end2 - begin2);
return 0;
}
运行结果:
cpp
10000次传值传参消耗的时间:166
10000次传引用传参消耗的时间:0
6.4.2 值和引用作为返回值类型的消耗对比:
cpp
struct A
{
int a[10000];
};
A a;
//直接返回
struct A func1()
{
return a;
}
//引用返回
struct A& func2()
{
return a;
}
int main()
{
struct A a;
int i = 0;
int begin1 = clock();
for (i = 0; i < 100000; i++)
{
func1();
}
int end1 = clock();
printf("100000次直接返回消耗的时间:%d\n", end1 - begin1);
int begin2 = clock();
for (i = 0; i < 100000; i++)
{
func2();
}
int end2 = clock();
printf("100000次引用返回消耗的时间:%d\n", end2 - begin2);
return 0;
}
运行结果:
cpp
100000次直接返回消耗的时间:198
100000次引用返回消耗的时间:2
注:传值和传引用在作为参数和返回值类型上消耗差距很大。
6.5 常引用
常引用的本质是对变量的权限进行约束。在引用的过程中,权限可以平移,权限可以缩小,但权限不能被放大。
cpp
//const是对a进行限制,a不能够再进行修改
const int a = 0;
//权限的放大,error C2440: "初始化": 无法从"const int"转换为"int &"
int& b = a;
//权限的平移,c和a一样都不可以进行修改
const int& c = a;
int m = 0;
//权限的缩小,m可以进行修改,但n不能直接进行修改
const int& n = m;
注:当权限缩小后,m可以进行修改,同时m的修改会间接影响n的变化。但n不能直接进行修改。
赋值不受引用权限的影响:
cpp
const int a = 0;
//这里的操作是赋值,而赋值是不会受到引用权限的影响的。
int b = a;
两个特殊案例:

6.6 引用和指针的区别
在语法上引用就是一个别名,和引用实体共用同一块空间。

但在底层实现上,引用其实是按照指针的方式进行实现的。
cpp
int a = 0;
int& ra = a;
int* pa = &a;
++(*pa);
++ra;

引用和指针在语法上的区别:
1.引用在语法概念上是定义一个变量的别名,指针在语法概念上是存储变量的地址。
2.引用在定义时必须要进行初始化,指针建议进行初始化,但在语法上不是必须的。
3.引用在初始化引用一个实体后,就不能再引用其他实体,而指针在任何时候都可以指向一个同类型的实体。
4.没有NULL引用,但有NULL指针。
5.在使用sizeof时的含义不同:sizeof引用的结果是计算引用的类型的大小,sizeof指针的结果则是和地址所占用字节个数(和平台有关)。
6.引用自加是对引用的实体增加1,指针自加则是偏移一个与实体相同类型的大小。
7.有多级指针,但没有多级引用。
8.访问实体的方式不同,指针需要显示解引用,引用编译器会自己处理。
9.指针很容易出现野指针和空指针的问题,引用则出现的比较少,引用比指针使用起来相对更加安全。
七、内联函数
7.1 概念
内联函数是C++对C语言中宏和函数优缺点的一种改进。在C语言中宏相较于函数在处理一些小段代码上效率较高,但宏又有由于操作符优先级的问题很容易出错,且无法进行调试且不会对类型进行检查等缺点。因此C++引入了内联函数,以inline修饰的函数被称之为内联函数,编译时C++会在调用内联函数的地方直接进行展开,没有函数调用时建立栈帧的开销,内联函数可以提升程序运行的效率。
内联函数展开的查看方式:
在release模式下,可以直接在汇编代码中查看内联函数的展开。
在debug模式下,需要对编译器进行设置,否则在汇编代码里无法看到对内联函数的展开。



cpp
inline int Add(int x, int y)
{
return x + y;
}
int ADD(int x, int y)
{
return x + y;
}
int main()
{
int a = Add(1, 2);
int b = ADD(1, 2);
return 0;
}

7.2 特性
1.inline是一种以空间换时间的做法,如果编译器将函数当做内联函数处理,在编译阶段,会用函数体替换函数调用。优点是少了函数调用过程中的开销,能提升效率,缺点是如果C++允许的内联函数的函数体较大,那么可能会导致代码膨胀的后果。
例如:

2.因此inline修饰的函数对编译器而言只是一个建议,不同的编译器关于inline的实现机制可能会有所不同。一般建议:将函数规模较小的(即函数体不是很长),不是递归,且调用频繁的函数采用inline进行修饰,否则编译器会直接忽略inline特性。下图为《C++Prime》第五版关于inline函数的建议:

3.内联函数不建议声明和定义分离,分离会导致链接错误。原因:内联函数会直接在使用的地方直接进行展开,因此不会在符号表生成内联函数的地址,链接期间查询符号表会找不到内联函数的地址,因此发生错误。解决方案:内联函数不要把声明和定义分离。
八、auto关键字
8.1 auto的功能
auto能够自动推导出类型,C++中有一个函数可以用于查看类型,typeid()。auto经常用于类型难以拼写等场景。
cpp
#include<vector>
#include<string>
int main()
{
int a = 0;
auto pa = &a;
auto& ra = a;
cout << typeid(pa).name() << endl;
cout << typeid(ra).name() << endl;
//常规下的auto没有太大的价值,当类型很长时auto会有很大的价值
std::vector<std::string> v;
std::vector<std::string>::iterator it = v.begin();
auto It = v.begin();
cout << typeid(it).name() << endl;
cout << typeid(It).name() << endl;
return 0;
}
运行结果:
cpp
int *
int
class std::_Vector_iterator<class std::_Vector_val<struct std::_Simple_types<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > > > >
class std::_Vector_iterator<class std::_Vector_val<struct std::_Simple_types<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > > > >
注:在使用auto定义变量时必须要对其进行初始化,因为编译器要在编译期间要根据初始化表达式来推导auto的实际类型。
8.2 auto不能推导的场景
1.auto不能作为函数参数
cpp
//auto不能作为形参,因为编译器无法对a是实际类型进行推导
void Func(auto a)
{
}
2.auto不能直接用来声明数组
cpp
//error C3318: "auto []": 数组不能具有其中包含"auto"的元素类型
auto array[] = { 1,2,3,4,5,6 };
九、基于范围的for循环
9.1 范围for的语法
对于一个有范围的集合而言,由程序员来说明循环的范围有时候可能会容易出错。因此C++11中引入基于范围的for循环。for循环后的内容由" : "分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
cpp
int array[] = { 1,2,3,4,5,6 };
//使用范围for遍历array数组
for (auto e : array)
{
cout << e << " ";
}
cout << endl;
注:与普通循环类似,范围for同样可以使用continue和break来结束循环或跳出循环。
9.2 范围for的使用条件和注意事项
1.范围for循环迭代的范围必须是确定的。
对于数组而言范围for循环的范围就是数组中第一个元素到最后一个元素之间的范围。对于类而言,必须提供例如begin和end的方法,begin和end就是范围for的循环迭代范围。
cpp
//此处的array代表的是首元素的地址,无法代表整个数组,因此范围for无法确定范围
void Func(int array[])
{
for (auto e : array)
{
cout << e << " ";
}
}
2.对数组而言范围for的功能是依次取出数组中的数据赋值给变量,并且自动判断结束和自动迭代。如果需要对范围内的数据进行修改等操作,需要用到引用。
cpp
int array[] = { 1,2,3,4,5,6 };
for (auto& e : array)
{
//对数组内的数据进行修改
e *= 2;
}
十、空指针
NULL实际上是一个宏,在对NULL的定义文件中,可以看到以下代码:
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到NULL在可能会被定义成为0,或者无类型指针(void*)的常量,在C++中使用NULL作为空值的指针可能会遇到一些麻烦。为了继续表示空指针,C++新推出了nullptr。
注:在使用nullptr表示空值时,不需要包含头文件。在C++11中sizeof(nullptr)和sizeof(void*(0))所占的字节数相同。