1. C++关键字
C语言有32个关键字,C++对其进行了扩充,引入了31个关键字,总共有63个
2. 命名空间
对于C/C++中,会存在函数名,变量名相同产生冲突的情况,于是可以使用命名空间,把可能会冲突的变量,函数,结构体等等放入一个空间,可以使用关键字namespace
关键字修饰这些标识符,
2.1 定义
定义命名空间,使用namespace
关键字,后接其名字+{}.
- 普通的命名空间
//1. 普通定义
namespace zt {
//可以定义变量
int a = 0;
//可以定义函数
int Add(int x, int y) {
return x + y;
}
// 可以定义结构体
struct data {
int year;
int mouth;
int day;
};
}
- 嵌套定义
//2. 嵌套定义
namespace zt1 {
int a = 0;
namespace zt2 {
// 不同空间的名字可以相同
int a = 0;
}
}
- 多次定义
// 3. 多处定义,在同一个工程中允许多个相同的命名空间,最后编译器会把这些合成到同一个命名空间
namespace zt {
int a = 0;
}
其实命名空间就是把全局域的一部分,只不过其内容是不可见的,需要通过特殊的访问方式才能访问.
2.2 使用
对于下面的命名空间该如何去访问其内部元素呢?
namespace zt {
int a = 10;
int b = 20;
int Add(int i, int j) {
return i + j;
}
}
- 加命名空间名称及作用域限定符
int main(){
// 加命名空间名称及作用域限定符
printf("%d", zt::a);
return 0;
}
- 使用using引用命名空间成员
using zt::b;
int main() {
printf("%d\n", b);
printf("%d\n", zt::a);
return 0;
}
- 使用using namespace 把指定命名空间展开
using namespace zt;
int main() {
printf("%d\n", a);
printf("%d\n", b);
printf("%d\n", Add(a, b));
return 0;
}
3. C++ 输入&输出
示例:
#include<iostream>
using namespace std;
int main() {
cout << "Hello world" << endl;
return 0;
}
-
向
cout标准输出(控制台)
和cin标准输入(键盘)
时,必须包含头文件和命名空间std -
<<
和>>
分别叫做流插入和流提取.对应输入和输出(可以看箭头流向观察)-
#include<iostream> 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; }
-
-
endl表示换行,利用这种方式输出数据时可以不用指定格式
-
#include<iostream> using namespace std; int main() { int a = 10; double c = 1.111; cout << a << c << endl; cout << "hello" << " C++" << '\n'; return 0; }
-
4. 缺省参数
4.1 概念
缺省参数是指在定义或者声明函数时
为函数参数指定一个默认值
.在调用该函数时,如果没有指定参数会使用默认值,否则使用指定参数.
示例:
#include<iostream>
using namespace std;
void fun(int a = 10) {
cout << a << endl;
}
int main() {
fun();
fun(1);
}
4.2 缺省参数分类
- 全缺省参数
函数形参全部设置为缺省参数
void fun1(int a = 10, int b = 20, int c = 30) {
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
- 半缺省参数
函数形参部分设置为缺省参数
void fun1(int a, int b = 20, int c = 30) {
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
注:
- 对于半缺省参数其参数必须从右向左依次设置,不能间隔设置
// error
void fun1(int a = 10, int b, int c = 30);
void fun1(int a, int b = 10, int c);
void fun1(int a = 10, int b, int c);
- 缺省参数不能在定义和声明中"同时出现"
// error
// test.h
void fun(int a, int b, int c = 10);
// test.c
void fun(int a, int b, int c = 10) {
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
正确用法是在这两处之一的位置设置即可
- 缺省值必须是常量或者全局变量
int b = 10;
void fun(int a = b) {
cout << a << endl;
}
5. 函数重载
C++允许在同一作用域中声明几个功能类似的同名函数 它们的函数名相同,但是函数形参列表不同
的情况,这样就可以根据调用者的需求达到不同的效果
int add(int x, int y) {
return x + y;
}
double add(double x, double y) {
return x + y;
}
int main() {
cout << add(1, 1) << endl;
cout << add(1.2, 2.3) << endl;
}
注: 函数类型,参数个数,参数顺序不同构成重载,返回值不同不会构成
5.1 函数重载的原理 - 名字修饰
为什么C++可以支持函数重载,而C语言不支持呢?
那么就要知道一个C/C++程序是如何通过编译器编译后运行起来的,其经历了下面4个阶段:预处理,编译,汇编,链接
对于一个函数我们在.h文件中声明,在.c文件中定义,在汇编后为了快速找到函数地址,会把地址保存在符号表中,对于.h文件中的函数由于没有定义那么这里的地址就是一个空地址,.c文件就保存了函数地址,所以在链接过程中将文件中的函数名,变量名等等进行连接,在合并时,将相同符号合并,取其合法地址填充.
对于C语言中的重载函数,形成的函数名相同,所以在形成符号表时存在歧义和冲突,链接时也会存在相同问题,因为它们都是直接使用函数名去标识和查找,而重载函数其名字相同使用会出错.
而C++不是直接通过函数名去标识和查找函数,而是它们有自己的函数名修饰规则(对于g++编译器下,函数修饰规则为_Z+函数长度+函数名+类型首字母
)所以在链接过程中重载函数的函数修饰名不同,就可以实现函数重载
5.2 extern “C”
在C++工程中如果需要采用C语言的风格来编译代码,可以在函数前加extern "C"
,这样编译器就可以按照C语言的风格进行编译
6. 引用
6.1 概念
引用不是创建一个新的变量,而是给已经存在的变量取了个别名,所以二者共用同一块内存空间.
语法格式:
类型& 引用变量名 = 引用实体
int main() {
int a = 10;
int& b = a;
cout << "修改前: " << a << endl;
cout << "修改前: " << b << endl;
b = 20; // 修改b的指向
cout << "修改后: " << a << endl;
cout << "修改后: " << b << endl;
}
注: 引用和实体的类型必须是相同的
6.2 引用特性
- 引用在定义时必须初始化
// error
int main() {
int a = 10;
int& b;
}
- 一个变量可以有多个引用
int main() {
int a = 10;
int& b = a;
int& c = b;
int& d = b; // a,b,c,d都是指向同一块空间
}
- 引用一旦引用一个实体,就不能在引用其他实体了
int main() {
int a = 10;
int& b = a;
// 当前b是a的引用
int c = 20;
// 那么这里是改变引用还是赋值呢?
b = c;
}
引用和实体的指向没有改变,改变的指向空间的数据
6.3 常引用
如果实体被const修饰,那么使用普通引用是不能够成功引用,二者访问权限应该相同
int main() {
// 权限变大 - 不行
const a = 10;
int& b = a;
//权限不变 - 可以
const int c = 20;
const int& d = c;
//权限变小 - 可以
int e = 30;
const int& f = e;
}
通过这种设置可以使编译器时刻检查程序员是否把引用交给一个低权限的实体.
小知识: 在C/C++等语言中存在整型提升的操作,那么它是如何提升的,如果把提升后的数据变成常引用可以成功吗?
// 整型提升
double d = 11.11;
int a = d;
// 原理是11.11这个浮点数其整数部分会被截断放入一个临时变量中,如何再把这个变量赋值给a
// 如果整型提升给常引用变量呢? 如果是const修饰的呢?
int& c = d; // false
const int& e = d; // ture
// 因为截断生成临时变量是作为右值赋给左值的,而临时变量是具有常性的,如果赋值给int&相当于权限变大,而赋值给const修饰的权限不变
6.4 应用场景
- 做参数
将参数作为返回结果,不需要返回
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main() {
int a = 10, b = 20;
Swap(a, b);
}
- 做返回值
对于作返回值不能是局部变量,原因是编译器不同对函数调用结束后是否销毁有很大关系,如果销毁可以接受,但是如果后面的程序又建立栈帧,可能会覆盖原来函数的栈帧,导致数据改变,从而导致引用返回值发生改变.所以最好使用全局变量或者static修饰的变量,它们的生命周期不会因为函数调用结束后而被污染
int& Add(int a, int b)
{
static int c = a + b;
return c;
}
6.5 引用和指针的区别
在底层逻辑上,引用的底层是按照指针方式来实现的
引用和指针的不同点:
引用
在定义时必须初始化,指针
建议初始化引用
在初始化指向一个实体后,就不能引用其他实体,而指针
可以在任意时刻指向不同的同类型实体- 对于
引用
没有NULL引用,但是指针
有null- sizeof中所表达的含义不同:引用(结果是引用类型的大小) 指针(结果是地址空间所占用的字节个数(和操作平台有关32位为4字节))
- 有多级指针,但是没有多级引用
- 访问实体不同,指针需要解引用,引用时编译器自己处理
- 引用比指针使用起来相对安全(不用判断空指针,野指针)
7. 内联函数
7.1概念
在C语言中如果对于一个函数经常使用,就会创建大量堆栈,导致效率低下,通常情况下,对于代码简短的函数比如Add(),swap()类似的函数,可以使用宏定义:
#define Add(x, y) ((x) + (y))
但是在C++有另一种优化方案,使用inline
关键字修饰,在C++编译器编译该函数时会在调用内联函数的地方展开 ,没有函数压栈的开销,从而提高效率
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式:
在release模式下,查看编译器生成的汇编代码中是否存在call Add
在debug模式下,需要对编译器进行设置,否则不会展开
7.2 特性
-
inline是一种以空间换时间的做法,省去调用函数额开销.所以代码很长或者有循环/递归的函数不适宜使用作为内联函数
-
inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联
-
inline不建议声明和定义分离,分离会导致链接错误.因为inline被展开,就没有函数地址了,链接就会找不到
8. auto关键字(C++11)
8.1 auto引入
在早期C/C++中auto关键字的含义是: 使用auto修饰的变量,是具有自动存储器的局部变量,但是很少使用
于是在C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
类型指示符: 根据赋值实体的类型自动推导,如JavaScript的var,let;Java中的var
这种推导类型关键字可以让程序员不必太过关心数据类型,但是不方便维护
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
// typeid().name 得到实体的具体类型
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
注: 使用atuo关键字定义变量时必须进行初始化,因为在编译过程中auto就相当于一个占位符,会在初始化时根据表达式类型来替换成其真正的类型.
8.2 auto使用场景
- auto与指针和引用结合起来使用
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
// 其实可以把atuo替换为int就很清楚该怎么书写了
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的初始化表达式类型不同
}
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用
8.2 auto不能推导的场景
- auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
- auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
9. for循环(C++11)
9.1 语法
在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;
}
而对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围
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来跳出整个循环
9.2 使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围
- 迭代的对象要实现++和==的操作
10. 指针空值nullptr(C++11)
在C++98/11中的指针初始化是使用NULL或者0:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}
NULL其实是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0**,或者被定义为无类型指针**(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如 :
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0); //f(int)
f(NULL);//f(int)
int* p = nullptr;
f(p);//f(int*)
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
麻烦,比如 :
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0); //f(int)
f(NULL);//f(int)
int* p = nullptr;
f(p);//f(int*)
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