C++ 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。
注释
- C相关方面不会过多描写
- 由于已经掌握多种面向对象语言(php, python, dart…),面向对象的概念等不会过多描写
- 由于设计模式可以更好的理解C++里面一些面向对象,在这里不会过多描写
- 单纯对C++的语法,和一些特有的对象进行记录
- 单纯为了记录的笔记,会很乱
常用库
1 2 3 4 5 6 7
| #include <iostream> #include <cmath> #include <cstdlib> #include <iomanip> #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] [=, &z]
|
对于[=]或[&]的形式,lambda 表达式可以直接使用 this 指针。但是,对于[]的形式,如果要使用 this 指针,必须显式传入:
1
| [this]() { this->someFunc(); }();
|
检查一个空指针
引用
与指针的区别:
- 不存在空引用。引用必须连接到一块合法的内存。
- 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
- 引用必须在创建时被初始化。指针可以在任何时间被初始化
- 引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据
- 引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它数据
创建引用
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 = y; y = temp; return; }
|
引用作为函数返回值
1 2 3 4
| double &setValues(int i) { return vals[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: ClassName() {}
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};
|
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 ); };
void printWidth( Box 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
| #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
| #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
|
关键字
派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 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
| 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.seekg( n );
fileObject.seekg( n, ios::cur );
fileObject.seekg( n, ios::end );
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(ExceptionName e2) { } catch(ExceptionName eN) { }
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::vector 和 std::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
运算符来为任意的数据类型动态分配内存
new
与 malloc()
函数相比,其主要的优点是,new
不只是分配了内存,它还创建了对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| new data-type;
double* pvalue = NULL; pvalue = new double;
double* pvalue = NULL; if( !(pvalue = new double )) { cout << "Error: out of memory." <<endl; exit(1); }
char* pvalue = NULL; pvalue = new char[20]; delete pvalue;
|
一维数组
1 2 3 4 5
| int *array=new int [m];
delete [] array;
|
二维数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int **array
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 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 2 3 4 5 6 7 8 9 10 11 12
| namespace namespace_name1 { namespace namespace_name2 { } }
using namespace namespace_name1::namespace_name2;
using namespace namespace_name1;
|
模板
泛型编程的基础
假设你要实现Queue数据结构,你不知道里面放什么。
定义一个模板类
使用此类型,即可实现能放任意元素的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 的字符串,它表示程序被编译的时间。 |