[学习笔记] - C++

C++ 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。

注释

  • C相关方面不会过多描写
  • 由于已经掌握多种面向对象语言(php, python, dart…),面向对象的概念等不会过多描写
  • 由于设计模式可以更好的理解C++里面一些面向对象,在这里不会过多描写
  • 单纯对C++的语法,和一些特有的对象进行记录
  • 单纯为了记录的笔记,会很乱

常用库

1
2
3
4
5
6
7
#include <iostream>  // cin、cout、cerr 和 clog
#include <cmath>
#include <cstdlib>
#include <iomanip> // 该文件通过所谓的参数化的流操纵器(比如 setw 和 setprecision),来声明对执行标准化 I/O 有用的服务。
#include <cstring>
#include <ctime>
#include <fstream> // 为用户控制的文件处理声明服务

基本的内置类型

类型 关键字
布尔型 bool
字符型 char
整型 int
浮点型 float
双浮点型 double
无类型 void
宽字符型 wchar_t

修饰符

  • signed
  • unsigned
  • short
  • long

类型限定符

限定符 含义
const const 类型的对象在程序执行期间不能被修改改变。
volatile 修饰符 volatile 告诉编译器不需要优化volatile声明的变量,让程序可以直接从内存中读取变量。对于一般的变量编译器会对变量进行优化,将内存中的变量值放在寄存器中以加快读写效率。
restrict 由 restrict 修饰的指针是唯一一种访问它所指向的对象的方式。只有 C99 增加了新的类型限定符 restrict。

存储类

存储类 含义
auto 声明变量时根据初始化表达式自动推断该变量的类型、声明函数时函数返回值的占位符
register 定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小
static 编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁
extern 提供一个全局变量的引用,全局变量对所有的程序文件都是可见的
mutable 允许对象的成员替代常量
thread_local (C++11) 说明符声明的变量仅可在它在其上创建的线程上访问

从 C++ 11 开始,auto 关键字不再是 C++ 存储类说明符,且 register 关键字被弃用。

函数与表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[capture](parameters)->return-type{body}
[capture](parameters){body}

// 例子
[](int x, int y) -> int { int z = x + y; return z + x; }
[](int x, int y){ return x < y ; }
[]{ ++global_x; }

// 变量传递 | 传值和传引用
[] // 沒有定义任何变量。使用未定义变量会引发错误。
[x, &y] // x以传值方式传入(默认),y以引用方式传入。
[&] // 任何被使用到的外部变量都隐式地以引用方式加以引用。
[=] // 任何被使用到的外部变量都隐式地以传值方式加以引用。
[&, x] // x显式地以传值方式加以引用。其余变量以引用方式加以引用。
[=, &z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用。

对于[=]或[&]的形式,lambda 表达式可以直接使用 this 指针。但是,对于[]的形式,如果要使用 this 指针,必须显式传入:

1
[this]() { this->someFunc(); }();

检查一个空指针

1
2
if(ptr)     /* 如果 ptr 非空,则完成 */
if(!ptr) /* 如果 ptr 为空,则完成 */

引用

与指针的区别:

  • 不存在空引用。引用必须连接到一块合法的内存。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 引用必须在创建时被初始化。指针可以在任何时间被初始化
  • 引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据
    • 引用类似于 Windows 中的快捷方式
  • 引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它数据

创建引用

1
2
3
4
int i = 17;

int &r = i;
double &s = d;

引用作为函数参数

将函数的形参指定为引用的形式,这样在调用函数时就会将实参和形参绑定在一起,让它们都指代同一份数据。

1
2
3
4
5
6
7
8
void swap(int &x, int &y)
{
int temp;
temp = x; /* 保存地址 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 x 赋值给 y */
return;
}

引用作为函数返回值

1
2
3
4
double &setValues(int i)
{
return vals[i]; // 返回第 i 个元素的引用
}
  • 不能返回局部数据

常引用

若不希望通过引用来修改原始的数据

1
2
const type &name = value;
type const &name = value;

面向对象

面向对象的本质就是设计模式,也就是代码重用。

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Box
{
public:
double length; // 盒子的长度
double breadth; // 盒子的宽度
double height; // 盒子的高度

double getVolume(void)
{
return length * breadth * height;
}
};

double Box::getVolume(void)
{
return length * breadth * height;
}
  • new创建类对象需要指针接收,一处初始化,多处使用
  • new创建类对象使用完需delete销毁
  • new创建对象直接使用堆空间,而局部不用new定义类对象则使用栈空间
  • new对象指针用途广泛,比如作为函数返回值、函数参数等
  • 频繁调用场合并不适合new,就像new申请和释放内存一样

构造函数

  • 成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败
  • const 成员或引用类型的成员。因为 const 对象或引用类型只能初始化,不能对他们赋值(类似dart里面的final)
  • 不管是声明还是定义,函数名前面都不能出现返回值类型,即使是 void 也不允许
  • 函数体中不能有 return 语句

  • 传入参数方法一
1
2
3
ClassName::ClassName(int x, int y) : attribute1(x), attribute(y)
{
}

参数初始化顺序与初始化表列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关

  • 传入参数方法二
1
2
3
4
5
6
7
8
9
10
11
12
class ClassName
{
public:
ClassName(int);
private:
int my_x;
}

ClassName::ClassName(int x)
{
my_x = x;
}
  • 传入参数方法三(构造函数的重载)
1
2
3
4
5
6
7
8
9
10
11
12
class ClassName
{
int my_m;
public:
// default
ClassName() {}

// not default
ClassName(int m) {
my_m = m;
}
};
  • 传入参数方法四
1
2
3
4
5
6
7
8
9
10
11
12
13
class ClassName
{
int a;
int b;

public:
ClassName(int i, int j){
a = i;
b = j;
}
};

Test t{0,0}; // C++11 only,相当于 Test t(0,0);
  • 传入参数方法五
1
2
3
4
5
6
7
8
9
10
11
12
class ClassName
{
public:
ClassName() : a(x), b(y)
{
}

private:
int a;
int b;
};

析构函数

类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。

1
2
3
4
5
6
7
8
9
10
class ClassName
{
public:
~ClassName(); // 这是析构函数声明
};

ClassName::~ClassName(void)
{
cout << "Object is being deleted" << endl;
}

友元函数

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Box
{
double width;
public:
double length;
friend void printWidth( Box box );
void setWidth( double wid );
};

// 请注意:printWidth() 不是任何类的成员函数
void printWidth( Box box )
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width <<endl;
}
  • 友元函数没有this指针
  • 要访问非static成员时,需要对象做参数
  • 要访问static成员或全局变量时,则不需要对象做参数
  • 如果做参数的对象是全局对象,则不需要对象做参数.
  • 可以直接调用友元函数,不需要通过对象或指针
  • 友元函数不仅可以是全局函数(非成员函数),还可以是另外一个类的成员函数

类作为友元

  • 例如将类 B 声明为类 A 的友元类,那么类 B 中的所有成员函数都是类 A 的友元函数
    • 可以访问类 A 的所有成员,包括 public、protected、private 属性的
    • 友元的关系是单向的而不是双向的
    • 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类
  • 类作为友元需要注意的是友元类和原始类之间的相互依赖关系
    • 如果在友元类中定义的函数使用到了原始类的私有变量,那么就需要在友元类定义的文件中包含原始类定义的头文件。
    • 但是在原始类的定义中(包含友元类声明的那个类),就不需要包含友元类的头文件
    • 也不需要在类定义前去声明友元类,因为友元类的声明自身就是一种声明(它指明可以在类外找到友元类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//A.h
#pragma once
#include <iostream>
using namespace std;
class A
{
friend class B;
public:
~A(void);
static void func()
{
cout<<"This is in A"<<endl;
}
private:
A(){};
static const A Test;
};
1
2
3
4
5
6
7
8
//B.h
#pragma once
class B
{
public:
B(void);
~B(void);
};

类与const关键字

  • const成员变量
    • 用法和普通const变量的用法相似,只需要在声明时加上const关键字
    • 初始化const成员变量只有一种方法,就是通过参数初始化表
  • const成员函数
    • const成员函数可以使用类中的所有成员变量,但是不能修改它们的值
    • 这种措施主要还是为了保护数据而设置的,const成员函数也称为常成员函数。
  • const对象
    • const 也可以用来修饰对象,称为常对象
    • 一旦将对象定义为常对象之后,就只能调用类的 const 成员了

static静态成员函数

  • 在类中,static除了可以声明静态成员变量,还可以声明静态成员函数
    • 静态成员函数只能访问静态成员。
    • 静态成员函数可以通过类来直接调用
    • 编译器不会为它增加形参this,它不需要当前对象的地址

运算符

  • :::运算符之前必须使用类名
  • .:调用实例成员

继承

已有的类称为基类,新建的类称为派生类。

1
class derived-class: access-specifier base-class

关键字

  • public
  • private
  • protected

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。
我们可以根据访问权限总结出不同的访问类型,如下所示:

访问 public protected private
同一个类
派生类 ×
外部 × ×

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

继承类型

  • 公有继承(public):
    • 基类的公有成员也是派生类的公有成员
    • 基类的保护成员也是派生类的保护成员
    • 基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
  • 保护继承(protected):
    • 基类的公有和保护成员将成为派生类的保护成员。
  • 私有继承(private):
    • 基类的公有和保护成员将成为派生类的私有成员。

多继承

1
2
3
4
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};

重载函数

在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class printData
{
public:
void print(int i) {
cout << "整数为: " << i << endl;
}

void print(double f) {
cout << "浮点数为: " << f << endl;
}

void print(char c[]) {
cout << "字符串为: " << c << endl;
}
};

运算符重载

简单来说就是类似python里面__add__啥的魔法方法了
载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。

1
2
Box operator+(const Box&);
Box operator+(const Box&, const Box&);
  • 运算重载符不可以改变语法结构。
  • 运算重载符不可以改变操作数的个数。
  • 运算重载符不可以改变优先级。
  • 运算重载符不可以改变结合性。

可重载运算符

运算符 具体
双目算术运算符 + (加),-(减),*(乘),/(除),% (取模)
关系运算符 ==(等于),!= (不等于),< (小于),> (大于>,<=(小于等于),>=(大于等于)
逻辑运算符 丨丨 (逻辑或),&&(逻辑与),!(逻辑非)
单目运算符 + (正),-(负),*(指针),&(取地址)
自增自减运算符 ++(自增),--(自减)
位运算符 丨 (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移)
赋值运算符 =, +=, -=, *=, /= , % = , &=, 丨=, ^=, <<=, >>=
空间申请与释放 new, delete, new[ ] , delete[]
其他运算符 ()(函数调用),->(成员访问),,(逗号),[](下标)

this指针

1
2
3
4
5
6
7
8
9
10
11
12
class ClassName
{
public:
ClassName(int);
void func(){
cout << my_x << endl;
cout << this->my_x << endl;
cout << (*this).my_x << endl;
}
private:
int my_x;
}

类似python里的self

多态

同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果
本质:基类的引用指向子类的对象。

参照设计模式:工厂模式

虚函数

基类中使用关键字 virtual 声明的函数,会告诉编译器不要静态链接到该函数。
也就是:动态链接、后期绑定

纯虚函数

基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

1
2
// pure virtual function
virtual void function() = 0;

~= 0告诉编译器,函数没有主体,上面的虚函数是纯虚函数。

抽象类

如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。
它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。
可用于实例化对象的类被称为具体类。

文件和流

数据类型 描述
ofstream 该数据类型表示输出文件流,用于创建文件并向文件写入信息。
ifstream 该数据类型表示输入文件流,用于从文件读取信息。
fstream 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。

打开文件

1
void open(const char *filename, ios::openmode mode);
模式标志 描述
ios::app 追加模式。所有写入都追加到文件末尾。
ios::ate 文件打开后定位到文件末尾。
ios::in 打开文件用于读取。
ios::out 打开文件用于写入。
ios::trunc 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <fstream>
#include <iostream>
using namespace std;

int main ()
{
char data[100];

// 以写模式打开文件
ofstream outfile;
outfile.open("afile.dat");

cout << "Writing to the file" << endl;
cout << "Enter your name: ";
cin.getline(data, 100);

// 向文件写入用户输入的数据
outfile << data << endl;

cout << "Enter your age: ";
cin >> data;
cin.ignore();

// 再次向文件写入用户输入的数据
outfile << data << endl;

// 关闭打开的文件
outfile.close();

// 以读模式打开文件
ifstream infile;
infile.open("afile.dat");

cout << "Reading from the file" << endl;
infile >> data;

// 在屏幕上写入数据
cout << data << endl;

// 再次从文件读取数据,并显示它
infile >> data;
cout << data << endl;

// 关闭打开的文件
infile.close();

return 0;
}

文件位置指针

1
2
3
4
5
6
7
8
9
10
11
// 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
fileObject.seekg( n );

// 把文件的读指针从 fileObject 当前位置向后移 n 个字节
fileObject.seekg( n, ios::cur );

// 把文件的读指针从 fileObject 末尾往回移 n 个字节
fileObject.seekg( n, ios::end );

// 定位到 fileObject 的末尾
fileObject.seekg( 0, ios::end );

异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try
{
// 保护代码
}
catch(ExceptionName e1)
{
// catch 块
}
catch(ExceptionName e2)
{
// catch 块
}
catch(ExceptionName eN)
{
// catch 块
}

throw "Division by zero condition!";

标准的异常

异常 描述
std::exception 该异常是所有标准 C++ 异常的父类。
std::bad_alloc 该异常可以通过new抛出。
std::bad_cast 该异常可以通过 dynamic_cast 抛出。
std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid 该异常可以通过 typeid 抛出。
std::logic_error 理论上可以通过读取代码来检测到的异常。
std::domain_error 当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument 当使用了无效的参数时,会抛出该异常。
std::length_error 当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range 该异常可以通过方法抛出,例如 std::vectorstd::bitset<>::operator[]()
std::runtime_error 理论上不可以通过读取代码来检测到的异常。
std::overflow_error 当发生数学上溢时,会抛出该异常。
std::range_error 当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error 当发生数学下溢时,会抛出该异常。

定义新的异常

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <exception>
using namespace std;

struct MyException : public exception
{
const char * what () const throw ()
{
return "C++ Exception";
}
};

动态内存

new 运算符来为任意的数据类型动态分配内存
newmalloc() 函数相比,其主要的优点是,new 不只是分配了内存,它还创建了对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new data-type;

double* pvalue = NULL; // 初始化为 null 的指针
pvalue = new double; // 为变量请求内存

double* pvalue = NULL;
if( !(pvalue = new double ))
{
cout << "Error: out of memory." <<endl;
exit(1);
}

char* pvalue = NULL; // 初始化为 null 的指针
pvalue = new char[20]; // 为变量请求内存
delete pvalue; // 释放 pvalue 所指向的内存

一维数组

1
2
3
4
5
// 动态分配,数组长度为 m
int *array=new int [m];

//释放内存
delete [] array;

二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int **array
// 假定数组第一维长度为 m, 第二维长度为 n
// 动态分配空间
array = new int *[m];
for( int i=0; i<m; i++ )
{
array[i] = new int [n] ;
}
//释放
for( int i=0; i<m; i++ )
{
delete [] arrary[i];
}
delete [] array;

命名空间

在不同库里可能有相同的变量名,在链接的时候会出现问题。所以需要命名空间

定义命名空间

1
2
3
namespace namespace_name {
// 代码声明
}

调用带有命名空间的函数或变量

1
name::code;  // code 可以是变量或函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
int main ()
{
// 调用第一个命名空间中的函数
first_space::func();

// 调用第二个命名空间中的函数
second_space::func();

return 0;
}

using 指令

使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
using namespace first_space;
int main ()
{
// 调用第一个命名空间中的函数
func();

return 0;
}

特定引用

1
using std::cout;

嵌套的命名空间

1
2
3
4
5
6
7
8
9
10
11
12
namespace namespace_name1 {
// 代码声明
namespace namespace_name2 {
// 代码声明
}
}

// 访问 namespace_name2 中的成员
using namespace namespace_name1::namespace_name2;

// 访问 namespace:name1 中的成员
using namespace namespace_name1;

模板

泛型编程的基础

假设你要实现Queue数据结构,你不知道里面放什么。
定义一个模板类

1
template <class T>

使用此类型,即可实现能放任意元素的Queue。

函数模板

模板函数定义的一般形式如下所示:

1
2
3
4
template <class type> ret-type func-name(parameter list)
{
// 函数的主体
}

类模板

1
2
template <class type> class class-name {
}

在类模板外部定义成员函数的方法为:

  • type 是占位符类型名称,可以在类被实例化的时候进行指定
  • 您可以使用一个逗号分隔的列表来定义多个泛型数据类型
1
2
template<模板形参列表> 函数返回类型 类名<模板形参名>::函数名(参数列表){函数体}
template<class T1,class T2> void A<T1,T2>::h(){}

预处理器

## 运算符

## 运算符用于连接两个令牌。下面是一个实例:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

#define concat(a, b) a ## b
int main()
{
int xy = 100;

cout << concat(x, y);
return 0;
}

预定义宏

描述
__LINE__ 这会在程序编译时包含当前行号。
__FILE__ 这会在程序编译时包含当前文件名。
__DATE__ 这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。
__TIME__ 这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。