1. C++的新特性
1.0 输入输出
C++ 可以使用输入输出流 (std::cin, std::cout) 进行输出,比如 std::cout << "Hello World"; 需要包含 头文件 #include <iostream>
文件的输入输出, 使用ofstream和ifstream
int main () {
std:: string str = "Hello World!" ;
std:: ofstream fout ("out.txt" );
fout << str << std:: endl;
std:: ifstream fin ("out.txt" );
std:: string str1;
std:: string str2;
fin >> str1 >> str2;
return 0 ;
}
1.1 变量和动态内存分配
C++中的变量类型
global variable 全局变量,存储在 全局变量区
可以在不同的cpp文件之间共享,可以使用关键字 extern 来使用别的cpp文件中的全局变量
static global variable 静态全局变量 ,不能在cpp文件之间共享
local variable 存储在 栈 区上
static local variable 静态局部变量
存储在全局变量区
在初次使用的时候初始化,keeps its value between visit to the function
allocated variable 动态分配的变量
std:: string s = "hello" ;
std:: string* ps = & s;
(* ps ).length ();
ps -> length ();
C++动态内存分配
new用于动态分配内存给变量,如 new int , new double[1000]
与malloc的区别: malloc不执行类的构造函数 ,而new出新的对象的时候会执行对象的构造函数 new Class_Name[x] 会执行x次Class_Name的构造函数
int * leaked = new int ;
* leaked = 123 ;
leaked = new int ;
delete leaked;
int * p = new int ;
* p = 123 ;
delete p;
p = new int ;
delete p;
上面反例中一开始为指针 leaked 分配了一段内存空间并赋值了 123,但是第三行代码又为 leaked 赋值了一段新的内存空间,原来的内存空间已经没有指针指向,因此 不能访问,也不能删除 ,造成了内存泄漏
delete用于删除动态分配的内存
用法 delete p; delete[] p;
和 new 类似,delete 会执行所删除对象的析构函数,不能 delete 没有通过 new 得到的对象,同一块动态内存不能 delete 两次
两个指针p1,p2指向同一个数据,如果p1被delete了,p2也不能访问原本p1指向的变量的值,因为delete删除的是内存里的数据
int * p1 = new int ;
int * p2 = p1;
* p2 = 1 ;
delete p1;
p1 = nullptr;
p2 = nullptr;
指向同一个数据的两个指针实际上只是两个不同的变量名而已,delete删除的不是变量名而是数据
1.2 引用(Reference)
a new type in C++, 相当于给变量取了一个别名,使用方法为 type &refname = name; 引用的对象 不能 是表达式
引用和指针的区别
不能定义 空引用 ,引用必须连接到一块合法的内存
一旦引用被初始化为一个对象 就不能更改
引用在创建的时候必须要 初始化
int * f (int * x) {
(* x)++ ;
return x;
}
int & g (int & x) {
x++ ;
return x;
}
int x;
int & h () {
return x;
}
int main () {
int a = 0 ;
f (& a);
g (a);
h () = 16 ;
}
1.3 const类型
用于定义常量类型,如 const int x=12345;, const类型的变量在初始化之后就不能改变其值,const型变量不能在连接单元外使用
Run-time constants 运行时常量
数组的定义时的长度值必须在 编译期 就已知,所以宏定义中的常数可以作为数组长度,而 int n; int a[n] 这样的语法不是标准 C++17。某些编译器可能把它作为扩展放行,但不应依赖。
const int size = 100 ;
int a[size];
int x;
std:: cin >> x;
const int runtime_size = x;
const和指针pointer
常量指针: char s[] = "abc"; char * const p = s; 不能赋予这个指针新的地址 ,相当于地址是 const 类型,但是 p 指向的值可以改变
const char* p = "abc"; 这种情况下p指向的是一个const char类型的值,因此 指向的值不能改变 ,而指针指向的对象可以改变
char s[] = "abc" ;
char * const q = s;
* q = 'c' ;
const char * p = "abc" ;
std:: string name = "zyc" ;
std:: string other = "xjn" ;
const std:: string* pointer_to_const = & name;
pointer_to_const = & other;
std:: string const * also_pointer_to_const = & name;
std:: string* const const_pointer = & name;
* const_pointer = "gzy" ;
不能将const类型的变量赋值给对应的指针,因为可能会带来const变量的改变,这是const类型的变量不允许的
可以把非const类型的值赋给对应的const型变量,函数中可以将参数设置为const类型表明这些参数在函数中不能被修改原本的值,也可以将返回值类型设置为const表示返回值不能被修改
struct Student {
int id ;
};
void foo (const Student* ps ) {
std:: cout << ps -> id << std:: endl;
std:: cout << (* ps ).id << std:: endl;
}
void bar (const Student& s) {
std:: cout << s.id << std:: endl;
}
int main () {
Student s;
s.id = 2 ;
foo (& s);
return 0 ;
}
char * s="Hello World!";实质上是 const char * 类型,不要去修改s中的内容,这是一种 未定义的行为 (undefined behavior),应该写成 char s[]="Hello World!";
int main () {
const char * s1 = "Hello World" ;
const char * s2 = "Hello World" ;
std:: cout << static_cast< const void *> (s1) << std:: endl;
std:: cout << static_cast< const void *> (s2) << std:: endl;
return 0 ;
}
2. 类 class
2.0 类的基本概念
C++中的对象=属性+操作 objects=attributes+operations,对于面向对象的编程而言,任何东西都是一个对象,任何对象都有对应的类型,程序就是一系列对象互相之间传递信息来完成功能
C++的类包含成员变量和成员函数
C++中class的定义,具体实现和调用可以分成三个文件
头文件header是具体实现和调用之间的接口,每个类的定义需要用1个头文件
::操作符可以用来访问类中的内容
2.1 构造函数和析构函数
struct Person {
char * name;
explicit Person (const char * s) {
name = new char [std:: strlen (s) + 1 ];
std:: strcpy (name, s);
}
Person (const Person& other) {
name = new char [std:: strlen (other.name ) + 1 ];
std:: strcpy (name, other.name );
}
Person& operator= (const Person& other) {
if (this == & other) {
return * this ;
}
char * new_name = new char [std:: strlen (other.name ) + 1 ];
std:: strcpy (new_name, other.name );
delete[] name;
name = new_name;
return * this ;
}
~ Person () {
delete[] name;
}
};
Person bar (const char * s) {
std:: cout << "in bar()" << std:: endl;
return Person (s);
}
int main () {
Person p1 ("Trump" );
Person p2 = p1;
std:: cout << p1.name << " at " << static_cast< void *> (p1.name ) << std:: endl;
std:: cout << p2.name << " at " << static_cast< void *> (p2.name ) << std:: endl;
return 0 ;
}
2.2 static类型
类中的成员(变量和函数)分为两种
静态成员:在类内所有对象之间共享
实例成员:只能在某个具体的对象中调用
静态函数不能访问实例成员
之前已经讲到了static类型的全局变量只在当前文件有效,不能通过extern跨文件调用,而函数中的static类型的变量在 第一次调用 的时候会被初始化,之后再调用该函数这个static类型的变量 保持上一次函数调用结束时的值
class A {
public :
A () { std:: cout << "A::A()" << std:: endl; }
~ A () { std:: cout << "A::~A()" << std:: endl; }
};
void f (int n) {
if (n > 10 ) {
static A a;
}
std:: cout << "f()" << std:: endl;
}
int main () {
std:: cout << "start" << std:: endl;
f (1 );
f (11 );
return 0 ;
}
2.3 Inline Function内联函数
需要在函数名前面加关键字 inline
内联函数在编译期会被编译器在调用处直接扩展为一个完整的函数,因此可以减少运行时调用函数的cost
内联函数的定义和函数主体部分都应该写在 头文件 中
本质是空间换时间
在 class 定义内部直接给出函数体的成员函数是 隐式 inline 的
an inline function is expanded in place, like a preprocessormacro, so the overhead of the function call is eliminated,不需要函数调用产生的开销,由编译器 直接优化 ,但是可能会使得需要编译的代码量增大(虽然写的人是看不出来的),主要作用是减小函数调用时的开销,一般在函数比较小的时候才会使用
2.4 继承Inheritance
2.5 友元 friend
友元函数
在类中声明一个全局函数或者其他类的成员函数为 friend
可以使这些函数拥有访问类内private和protected类型的变量和函数的权限
友元函数也可以是一个类,这种情况下被称为是友元类,整个类和所有的成员都是友元
友元函数本身不是那个类的成员函数,函数签名里不需要 className::来表示是这个类的成员函数,直接作为普通函数即可
class A {
public :
explicit A (int value) : val (value) {
std:: cout << "A()" << std:: endl;
}
friend void showValue (A a);
private :
int val;
};
void showValue (A a) {
std:: cout << a.val << std:: endl;
}
2.6 多态和虚函数
多态 Polymorphism
同一段代码可以产生不同效果
对于继承体系中的某一系列同名函数,不同的类型会调用不用的函数
一般情况下,有继承关系的类之间有函数构成重载关系,依然会根据变量类型来调用对应的函数,比如:
class A {
public :
virtual void foo () {
std:: cout << 1 << std:: endl;
}
};
class B : public A {
public :
void foo () override {
std:: cout << 2 << std:: endl;
}
};
int main () {
A a;
B b;
a.foo ();
b.foo ();
return 0 ;
}
此时运行的结果是1和2,即A型的变量的foo函数是基类中的,B类型的变量的foo函数是派生类中的
静态链接
函数的调用在程序开始运行之前就已经确定了
对于像下面这样的情况,基类的指针(引用)指向派生类,并且调用了基类中也存在的同名函数,最终调用的都是基类的同名函数class Shape {
public :
Shape (int a = 0 , int b = 0 ) : width (a), height (b) {}
int area () {
std:: cout << "Parent class area :" << std:: endl;
return 0 ;
}
protected :
int width;
int height;
};
class Rectangle : public Shape {
public :
Rectangle (int a = 0 , int b = 0 ) : Shape (a, b) {}
int area () {
std:: cout << "Rectangle class area :" << std:: endl;
return width * height;
}
};
class Triangle : public Shape {
public :
Triangle (int a = 0 , int b = 0 ) : Shape (a, b) {}
int area () {
std:: cout << "Triangle class area :" << std:: endl;
return width * height / 2 ;
}
};
int main () {
Shape* shape;
Rectangle rec (10 , 7 );
Triangle tri (10 , 5 );
shape = & rec;
shape-> area ();
shape = & tri;
shape-> area ();
return 0 ;
}
虚函数 Virtual Function
一种用于 实现多态 的机制,核心理念是通过基类访问派生类定义的函数
这种方式称为 动态 链接
用于区分派生类中和基类同名的方法函数,需要将 基类 的成员函数类型声明为virtual
如果会通过基类指针删除派生类对象,基类中的 析构函数必须为虚函数 ,否则会出现对象释放错误
纯虚函数: virtual int func() = 0; 表明该函数没有主体,基类中没有给出有意义的实现方式,需要在派生类中进行扩展
override 语法:派生类中可以用 override 关键字来声明,表示对基类虚函数的重写(override),不是重载(overload)
虚函数需要借助指针和引用达到多态的效果
如果基类指针/引用 指向基类 ,那就正常调用基类的相关成员函数
如果基类指针 指向派生类 ,则调用的时候会调用派生类的成员函数
派生类指针 不能 指向基类
class A {
public :
virtual ~ A () = default ;
virtual void foo () {
std:: cout << "A" << std:: endl;
}
};
class B : public A {
public :
void foo () override {
std:: cout << "B" << std:: endl;
}
};
int main () {
B b;
A* a = & b;
a-> foo ();
return 0 ;
}
虚函数的实现方式: 虚函数表 virtual table
每一个有虚函数的类都会有一个虚函数表,该类的任何对象中都存放着虚函数表的指针,虚函数表中 列出了该类的虚函数地址
虚函数表是一个指针数组,里面存放了一系列虚函数的指针
虚函数的调用需要经过虚函数表的查询,非虚函数的调用不需要经过虚函数表
虚函数表在代码的编译阶段就完成了构造
一个类只有一张虚函数表,每一个对象都有指向虚函数表的一个指针 __vptr
多态的函数调用语句被编译成一系列根据基类指针所指向的对象存放的虚函数表的地址,在从虚函数表中查找地址调用对应的虚函数
虚函数通常由编译器通过虚函数表等机制实现,但 __vptr、指针大小等属于具体实现细节,不是 C++17 标准保证
接口(C++抽象类)
描述了一个类应该有的功能和行为,但是不用在这个类中实现,而是在派生类中实现
可以使用纯虚函数来实现抽象类的定义,比如:
class Shape {
public :
Shape (int a, int b) : length (a), width (b) {}
virtual ~ Shape () = default ;
virtual double getArea () = 0 ;
protected :
int length;
int width;
};
class Rectangle : public Shape {
public :
Rectangle (int a, int b) : Shape (a, b) {}
double getArea () override {
return length * width;
}
};
class Triangle : public Shape {
public :
Triangle (int a, int b) : Shape (a, b) {}
double getArea () override {
return length * width / 2 ;
}
};
int main () {
Rectangle rectangle (10 , 7 );
Triangle triangle (10 , 5 );
Shape* shape = & rectangle;
std:: cout << shape-> getArea () << std:: endl;
shape = & triangle;
std:: cout << shape-> getArea () << std:: endl;
}
2.7 虚函数使用总结
情况1:基类和派生类都不是virtual
此时对于基类的对象和基类的指针,执行的就是基类的f,对于派生类的执行的就是派生类的f
class A {
public :
void f () {
std:: cout << "af" << std:: endl;
}
};
class B : public A {
public :
void f () {
std:: cout << "bf" << std:: endl;
}
};
int main () {
A a;
B b;
a.f ();
b.f ();
A* pb = & b;
pb-> f ();
}
class A {
public :
virtual void f (int i) {
std:: cout << 1 << std:: endl;
}
};
class B : public A {
public :
virtual void f (double i) {
std:: cout << 2 << std:: endl;
}
};
int main () {
B b;
A* pa = & b;
pa-> f (1 );
return 0 ;
}
这里输出的结果是1,事实上两个f并不构成虚函数的关系,因为f(1)中1是int类型,所以优先调用了对int匹配度高的
事实上如果是f(1.1)输出的结果仍然是1,并且CLion会提示参数需要类型转换
事实上两个f不构成虚函数的多态关系,所以调用哪个并不看指针指向的对象,而是看指针本身的类型!
2.8 强制类型转换
2.8.1 static_cast
static_cast 用于数据类型的强制转换,有这样几种用法
基本数据类型的转换,比如char转换成int
在类的体系中把基类和派生类的指针和引用进行转换
向上转换是安全的
向下转换是不安全的
只能在有相互联系的类型中进行相互转换,不一定包含虚函数
把空指针转换成目标类型的空指针
把任何类型转换成void类型
static_cast不能转换掉有const的变量
2.8.2 const_cast
const_cast 可以强制去掉const的常数特性, 只能用在指针和引用 上面
常量指针被转化成非常量的指针,仍然指向原来的对象
常量引用被转换成为非常量的引用,仍指向原来的对象
来看一段代码
如果原对象本来就是 const,去掉 const 后再写入是未定义行为,如果原对象不是 const,只是临时用 const 指针/引用观察它,则可以用 const_cast 恢复可写访问
const int a = 10 ;
const int * p = & a;
int * q = const_cast< int *> (p);
int b = 10 ;
const int * read_only = & b;
int * writable = const_cast< int *> (read_only);
* writable = 20 ;
std:: cout << b << std:: endl;
2.8.3 reinterpret_cast
reinterpret_cast 主要有三种用途
改变指针或者引用的类型
将指针或者引用转换成为足够长的整形
将整型编程指针或者引用类型
2.8.4 dynamic_cast
跟其他几个不同,其他几个都是编译时完成的, dynamic_cast 是在运行时进行类型检查的
不能用于内置的基本数据类型的强制转换
如果转换指针成功,将返回目标类型指针,指针转换失败会返回 nullptr。如果转换引用失败,会抛出 std::bad_cast
转换时基类一定要有虚函数,否则无法通过编译
原因是虚函数表名这个类希望可以用基类指针指向派生类,这样转换才有意义
在类的向上转换的时候,和 static_cast 效果一样,但是向下转换的时候比 static_cast 更安全,因此要求也更高
来看一段代码
class A {
public :
virtual ~ A () = default ;
};
class B : public A {};
int main () {
A a;
B b;
A* ap = & a;
if (dynamic_cast< B*> (ap)) {
std:: cout << "OK1" << std:: endl;
} else {
std:: cout << "Fail1" << std:: endl;
}
B* maybe_b = static_cast< B*> (ap);
std:: cout << maybe_b << std:: endl;
ap = & b;
if (dynamic_cast< B*> (ap)) {
std:: cout << "OK2" << std:: endl;
} else {
std:: cout << "Fail2" << std:: endl;
}
return 0 ;
}
运行的结果是第一个失败,其他的都成功
推测导致这个结果的原因:
当ap指向派生类的时候,进行强制类型转换变成派生类是可以成功的
当ap指向基类的时候, dynamic_cast 转换是否成功取决于指针指向的类型和即将转换的类型是不是一样,不一样就会失败,返回一个NULL,而static是可以成功的
其实是更安全的机制导致dynamic的检查更多,要求更高
3. 重载
3.0 函数的重载
多个同名函数的参数的个数或者类型不相同,这几个函数就构成重载关系
不能仅通过返回值类型的不同来形成函数的重载,事实上函数重载和返回值类型的关系不大
调用的时候优先调用匹配度最高的重载函数,重载的优先级是:
当没有参数类型恰好匹配的重载函数的时候,就会将参数进行隐式转换之后来寻找可以调用的重载函数,此时如果重载函数有多个符合条件,就会产生error
void foo (int a, int b) {
std:: cout << 1 << std:: endl;
}
void foo (double a, double b) {
std:: cout << 2 << std:: endl;
}
int main () {
foo (1 , 2 );
foo (1.0 , 2.0 );
}
3.1 运算符的重载
重载的一些基本性质
大部分运算符都可以重载
.、.*、::、?: 不能重载,sizeof、typeid、alignof 等也不是可重载运算符
不能重载不存在的运算符
必须对于整个class或者enumeration type进行运算符的重载
重载之后的运算符依然保持 原来一样的运算优先级和操作数的个数
运算符重载的基本语法
在类定义中对member function进行重载 return_type class_name::operator运算符(parameters) 此时参数为对应运算符所需参数-1
对外部函数进行重载 返回值类型 operator运算符(参数表) 此时参数个数和对应的运算符的所需个数相同
如果要访问类的private内容,需要声明这个重载函数为 友元
单目运算符一般声明为成员函数,双目运算符一般作为外部的函数
[ ] ( ) -> 等运算符必须作为成员函数
前缀的++和后缀的++
区别:后缀的++参数表是有个int类型的变量作为参数的
Integer& operator++ ();
Integer operator++ (int x);
比较大小关系的运算符重载时,可以通过 代码的复用 减少不必要的代码量
自定义的 类型转换
例如 Rational::operator double() const {};
隐式类型转换:
Single-argument constructors
implicit type conversion operators
避免隐式转换的方法:把单参数构造函数或转换函数声明为 explicit,表示不能进行隐式的类型转换
3.2 输入输出流的重载
stream的种类
Text stream 用ASCII码来解析,包括files和character buffers
Binary stream 二进制数据,no translation
stream的类型:
cin标准输入,使用流提取运算符>>,在C++标准库的istream中定义
cout标准输出,使用流插入运算符<<,在ostream中定义
cerr标准错误提示(unbuffered error)
clog标准错误提示(buffered error)
对>>和<<的重载
Has to be a 2-argument free function
std:: istream& operator>> (std:: istream& is, T& obj) {
return is;
}
std:: ostream& operator<< (std:: ostream& os, const T& obj) {
return os;
}
控制输出的格式,使用头文件 #include<iomanip>
其他的输入输出的方式:
int get() 支持单个字符读入
Returns the next character in the stream
Returns EOF if no characters left
get(char *buf, int limit, char delim='\n')
getline(char *buf, int limit, char delim='\n')
4. 模板
4.1 namespace 命名空间
使用命名空间来划分全局的各类类名可以避免名字的冲突,可以在不同的命名空间里定义相同的变量名
在引用的时候加上命名空间的限制符就可以了
或者也可以用 using namespace xxx 来说明程序中接下来用哪个命名空间中的东西
namespace space1 {
std:: string name = "randomstar" ;
}
namespace space2 {
std:: string name = "ToyamaKasumi" ;
}
int main () {
std:: cout << space1:: name << std:: endl;
using namespace space2;
std:: cout << name << std:: endl;
}
4.2 template编程
模板编程是一种复用代码的手段,是generic programming(泛型编程),把变量的类型当作参数来声明
函数模板:使用关键字template 后面加若干个变量作为类型声明一个函数模板,类型变量可以代替函数的返回值类型,参数类型和函数中的变量的类型
函数模板需要 实例化 (instantiation)之后再使用,如果没有被调用就不会被实例化
实例化是讲模板函数中的模板替换为对应的变量类型,然后生成一个对应的函数
函数模板在使用过程中不能发生参数和返回值的类型转换,必须要 类型完全匹配才能使用
函数重载时候的优先级
先找是否有完全匹配的普通函数
再找是否有完全匹配的模板函数
再找有没有通过进行隐式类型转换可以调用的函数
类模板
和函数模板差不多,template中声明的既可以是类内各种需要变量类型的地方
模板类也可以继承非模板类,也可以继承模板类(需要实例化)
非模板类可以继承自模板类(需要实例化)
Expression parameter
模板中的可以声明一些常数,class和typename的类型名可以有默认值default value,比如 template<typename T = int>
template < class T, int bounds = 100 >
class FixedVector {
public :
T& operator[](int i);
private :
T elements[bounds];
};
template < class T, int bounds>
T& FixedVector< T, bounds>:: operator[](int i) {
return elements[i];
}
FixedVector< int , 50 > v1;
FixedVector< int , 10 * 10 > v2;
FixedVector< int > v3;
4.3 STL和迭代器
5. Exceptions 异常处理
用于异常处理的语法
try{ } catch{ } :捕获throw抛出的异常
throw语句:抛出异常
throw exp; 抛出一个表达式:throw的参数可以是任何的表达式,表达式中的类型决定了抛出的结果的类型
throw; 只有在catch子句中有效,把原本捕捉到的异常抛出
异常处理的执行过程
程序按照正常的顺序执行,到达try语句,开始执行try内的保护段
如果在保护段执行期间没有发生异常,那么跳过所有的catch
如果保护段的执行期间有调用的任何函数中有异常,则可以通过throw创建一个异常对象并抛出,程序转到对应的catch处理段
首先要按顺序寻找匹配的catch处理器,如果没有找到,则 terminate( ) 会被自动调用,该函数会调用abort终止程序
如果在函数中进行异常处理并且触发了terminate,那么终止的是当前函数
如果找到了匹配的catch处理程序,并且通过值进行捕获,则其形参通过拷贝异常对象进行初始化,在形参被初始化之后,展开栈的过程开始,开始对对应的try块中,从开始到异常丢弃地点之间创建的所有局部对象的析构
C++中自带的异常的继承体系,定义在头文件 <exception> 中
what方法给出了产生异常的原因,是异常类之间都有的公共方法,已经被所有的子异常类重载
自定义的异常类:需要继承exception类
函数定义的异常声明
可以在函数名后面加 noexcept 关键字,说明该函数在运行的过程中 不抛出任何异常 ,如果还是产生了异常,就会调用 std::terminate 终止程序
C++17 中旧式动态异常说明(例如 double f(int, int) throw(int);)已经被移除,现代 C++ 使用 noexcept 表达「不抛出异常」
throw会导致一个函数没有执行完毕,但是在函数throw之前会执行所有 局部变量的析构函数
最好不要在析构函数中抛出异常
class T {
public :
T () {
std:: cout << "T()" << std:: endl;
}
~ T () {
std:: cout << "~T()" << std:: endl;
}
};
void foo () {
T t;
throw 1 ;
}
int main () {
try {
foo ();
} catch (...) {
std:: cout << "Catched!" << std:: endl;
}
return 0 ;
}