寒假学习了中国大学MOOC上崔毅东老师的C++课程,主要讲了C++的一些特点和C++11后的一些新东西,主要是一个大体的框架,没有特别深入某一方面,整理学习笔记如下:
面对对象(objiect-oriented)编程
一个对象表示现实中一个独一无二的实体1,其特征有四:A PIE
features of OOP |
---|
Abstraction(抽象) |
Polymorphism(多态) |
Inheritance(继承) |
Encapsulation(封装) |
继承
子类包含父类的大部分成员。子继承父,父泛化子。
- final标识符可以使得类不能被继承
class A final {};
- 继承基类的构造函数
class B : public A {
using A::A; //继承基类的构造函数
...
}
可以将派生类对象截断,只使用继承来的信息。如,B是A的派生类:
A a; B b;
a = b //正确
b = a //错误
多态
不同类型的实体或对象对于同一消息有不同的响应,就是多态性。
多态的表现形式包括但不限于:重载,重定义。
- 联编(binding)
确定具有多态性的语句调用哪个函数的过程。
静态联编:在编译时确定,如函数重载。
动态联编:在运行时确定,用动态联编实现的多态,称运行时多态。
modern C++
C++11的特性
typeid运算符
需要包含头文件<typeinfo>。
typeid运算符返回一个对象的引用auto& t = typeid(a);
typeid(a).name()返回a的类型名,其具体输出和编译器有关。
关键字auto
1.auto变量必须在定义时初始化。
auto a1 = 10; //正确
auto b1; //错误
b1 = 10;
2.定义在一个auto序列的变量必须始终推导成同一类型。
auto a2 = 10, a3{20}; //正确
auto b2{10}, b3 = 20.0; //错误
3.如果初始化表达式是引用或const,则去除引用或const语义,若要不去除语义,需给auto加上&号。
int a{10};
int &b = a;
auto a1 = b; //a1的类型是int而非int&
auto& b1 = b; //b1的类型是int&
const int c{10};
auto a2 = c; //a2的类型是int而非const int
auto& c1 = c; //c1的类型是const int
4.初始化表达式为数组时,auto推导类型为指针。
int a[2] = {1,2};
auto b1 = a;
cout << typeid(b1).name() << endl; //输出为int *
auto& b2 = a;
cout << typeid(b1).name() << endl; //输出为int [2]
5.C++14对""s进行了重载。
auto s1 = "Hello"; //s1是const char*类型
auto s2 = "World"s; //s2是string类
在C++14中,auto可以作为函数的返回值类型和参数类型。
关键字decltype() :declare type(利用已知类型声明类型)
int fun1() {return 2;}
auto fun2() {return 2.0;}
int main(){
decltype(fun1()) x;
decltype(fun1()) y;
cout << typeid(x).name() << endl; //输出int
cout << typeid(y).name() << endl; //输出double
return 0;
}
用using替代typedef
using UInt = unsigned int; //等价于 typedef unsigned int UInt;
UInt x = 42u;
风格:代表类型的名字要首字母大写,如上文的UInt。
定义模板的别名,只能使用using。
一元作用域解析运算符::
全局变量被局部变量隐藏时,可使用::访问全局变量。
int a = 10;
int main() {
int a = 20;
cout << a << endl; //输出20
cout << ::a << endl; //输出10
}
基于范围的for循环
要操作集合中的所有元素,只需要关心:
1.从集合中取出某个元素
2.保证所有元素都被遍历
此时就可以用到基于范围的for循环。
int a[5] = {1,2,3,4,5};
for(auto i : a){
cout << i << endl; //依次输出
}
for(auto& i : a){
i *= 2; //使用引用的方法改变数组元素的值
}
对象和类
this指针
this指针是一种特殊的内建指针,引用当前函数的调用对象。
在函数内访问类中被屏蔽的数据域时用到this指针。
如:类的构建函数的参数名与私有成员名相同(重名屏蔽)怎么办?
class Circle{
public:
Circle();
Circle(duble radius){
this->radius = radius; //两个radius,前者是该类中的私有成员,后者是形参
}
private:
double radius;
}
另一种解决重名屏蔽的简单方法是给构建函数的形参后加一个_
就地初始化
在classical C++中,只有静态常量整形成员才能在类中就地初始化。
class X {
static const int a = 1; //正确
const int b = 2; //错误,非static
static int c = 3; //错误,非const
static const char ch = 'A'; //错误,非int
}
而在C++11中非静态成员可以就地初始化。
静态成员
类中的静态成员是该类所有对象所共享的
class Circle{
private:
double radius;
static int ncircle; //统计创建对象的个数
public:
Circle() : Circle(1.0) {
//代理构造。无参构造函数调用有参构造函数
}
Circle(double radius){
this->radius = radius;
ncircle++;
}
~Circle(){
ncircle--; //析构函数
}
}
int Circle::ncircle; //需要声明
关键字:friend(友元)
C++独有的,C和JAVA都没有的概念。打破封装,可以访问私有成员。在运算符重载也有用到。
拷贝构造函数
用一个对象初始化另一个对象。在类中写如下的构造函数:
Circle(const Circle& e) //const可有可无
不写拷贝构造函数的时候,编译器会提供一个默认的拷贝构造函数,其默认为浅拷贝。默认的赋值运算符也是浅拷贝。
- 浅拷贝(shallow copy)和深拷贝(deep copy)
前提:数据域有指向其他对象的指针。
浅拷贝 | 深拷贝 |
---|---|
只拷贝指针的地址 | 拷贝指针指向的内容 |
class Person{
public:
Person(const Person& a) = default; //让编译器默认构建的浅拷贝ctor,不写也行
Person(const Person& a) { //深拷贝
birthday = new Date{a.birthday}; //birthday的类型是Date*
}
}
因为地址相同,浅拷贝会导致同时修改两个对象的数据。
虚函数
虚函数可以用父类指针访问子类对象成员。
class A{
public:
virtual string toString(){ return "A"; } //基类函数写为虚函数后,其子类的同名函数默认为虚函数
};
class B : public A{
public:
string toString() override { return "B"; } //override标识符用于确保虚函数同名,防止写错。若不加override即使派生类的同名函数名字写错,也不会报错
};
void print(A* p){
cout << p->toString() << endl; //调用哪个虚函数,不由指针类型决定,而由其指向的实际对象的类型决定
}
int main(){
A a;B b;
print(&a);print(&b); //输出A B,如果不加virtual关键字,则输出为A A
return 0;
}
该例中,如果通过基类指针没有找到对应的tos函数,则会沿着继承链向上找,调用最先找到的tos函数
若不使用虚函数,而用指针访问同名函数,则调用的类型取决于指针的类型:
A* p1;
p1 = a&;
string one = p1->tos();
p1 = &b;
string two = p1->tos();
cout << one << endl << two << endl; //因为p1是A*类型,所以one two都输出A
继承的访问控制属性
基类中的protected成员可以被派生类访问
派生类分为三种:
1.公有继承class Derived: public Base{};
2.私有继承class Derived: private Base{};
3.保护继承class Derived: protected Base{};
继承方式 | 基类成员在派生类中的访问属性 |
---|---|
共有继承 | 不变 |
私有继承 | 都变为private |
保护继承 | public成员变为protected |
共有继承可以通过派生类的对象访问从基类继承的公有成员。
抽象类与纯虚函数
类太抽象以至于无法实例化就叫做抽象类。
- 纯虚函数(抽象函数)
抽象函数要求子类必须实现它,包含抽象函数的类叫做抽象类。
在抽象类中定义抽象函数:virtual int func() = 0;
动态类型转化
使用dynamic_cast运算符。转化后就可以调用派生类中独有的函数。
Shape s;
Shape *p = &s;
Circle *c = dynamic_cast<Circle*>(p); //失败返回nullptr,转引用失败则抛异常
向上转型(子类向父类)可以不适用dynamic_cast而隐式转化。
输入输出流
需要支持C++17标准。
路径2
路径 | 解释 | 如 |
---|---|---|
绝对路径 | 包含完整的路径和驱动器符号 | c:\example\hello.txt |
相对路径 | 不包含驱动器及开头的\符号 | ./hello.txt |
namespace fs = std::filesystem //用namespace给名称空间filesystem起别名
fs::path p1("d:\\cpp\\hello.txt"); //反斜线 \ 需要转义
fs::path p2("d:/cpp/hello.txt"); //windows也支持/
格式化输出与IO流函数
包含<iomanip>,同样可用于文件操作。
设置域宽
cout << stew(3) << 'A'; //输出__A
数据所占的总字符数,只影响其后的第一个数据。其他控制符对其后的所有输入输出产生影响。
设置填充字符
数据超出域宽时,使用其进行填充。
cout << setfill('*') << setw(3) << 'A'; //输出**A
使用left或right表示左/右对齐。
cout << setfill('*') << setw(3) << left << 'A'; //输出A**
设置浮点精度
cout << setprecision(3) << 3.14159; //输出3.14
参数为数字总位数而非小数点后位数,后者用fixed表示。
cout << setprecision(3) << fixed << 3.14159; //输出3.142(会四舍五入)
对于没有小数部分的浮点数,用showpoint输出
double f = 3;
cout << f << endl; //输出3
cout << showpoint << f << endl; //输出3.00000
两个getline()函数
其一是输入流的成员函数getline(char* buf, int size, char delimiter)
至多读取size个字符存储到buf数组,以delimiter为结束符。
char name[10];
cin.getline(name, 10, '*');
int i = 0;
while(name[i] != '\0')
cout << name[i++] << " ";
其二是标准非成员函数std::getline(istream& is, string& str, char delimiter)
is是一个输入流对象的引用,str是存储输入string类对象的引用str,以delimiter为结束符。
string name;
getline(cin, name, '*');
int i = 0;
while(name[i] != '\0')
cout << name[i++] << " ";
fstream
创建fstream对象时,需指定文件打开模式。
模式 | 描述 |
---|---|
ios::in | 打开文件读数据 |
ios::out | 打开文件写操作 |
ios::app | 追加输出到文件末尾 |
ios::ate | 打开文件,光标移到末尾 |
ios::trunc | 若文件存在则舍弃其内容,是ios::out的默认行为 |
ios::binary | 打开文件以二进制模式读写 |
集中模式可以用按位或运算符|组合在一起。
s.open("xx.txt", ios::out | ios::app) //s是一个fstream对象
二进制输入输出
读/写 | 文本模式 | 二进制模式 |
---|---|---|
读 | >>,get(),getline() | read() |
写 | <<,put() | write() |
write()函数
可直接写入字符串。
fstream fs("xxx.txt", ios::binary | ios::trunc)
char s[] = "xxxxxx";
fs.write(s, 6); //第一个参数是char指针,第二个参数是其字节数
如何将非字符数据写入文件呢?首先将数据转化为字节序列,即字节流,然后再用write()函数。那么如何将信息转化为字节流呢?
reinterpret_cast将一种类型的地址转为另一种类型的地址。假设a是一个变量。
char* p1 = reinterpret_cast<char*>(&a)
但这样写入后打开txt文件显示是乱码,因为是以二进制形式写入,转化为ASCII码。
read()函数
用法与write()函数差不多,注意如果读取的字符没有结束符要将其设为’\0’。
文件位置指示器file positioner(fp)
注意不是文件指针,与指针完全是不同的概念,本身是一个长整型值。有时称为光标。
读写操作都是从文件位置指示器标记的位置开始的。打开文件时,fp指向文件头。读写文件时,fp会向后移动到下一个数据项(而非字节)。
通过fp的位置可随机访问,即读写文件的任意位置。
文件位置指示器 | 读 | 写 |
---|---|---|
获知其指向哪里 | tellg() | tellp() |
移动到指定位置 | seekg() | seekp() |
其中g意为get,p意为put。tell不需要参数,seek的用法如下:
int a[3] = {1,2,3};
fstream s;
s.open("hi.txt", ios::out | ios::in);
s << a[0] << a[1] << a[2]; //写入123
s.seekg(1, ios::beg); //将光标移到开始后的第一个字节,定位方向类型还有ios::end,ios::cur
int temp;
s >> temp;
cout << temp << endl; //将23读取到temp里,输入23
运算符函数与重载运算符
两条等价的语句,operator是C++的关键字。
cout << o1 + o2;
cout << o1.operator+(o2);
定义运算符函数。
Vec2D Vec2D::operator+(Vec2D addend){
return this->add(addend); //add是Vec2D的加法函数,其返回一个Vec2D类型的匿名对象
}
可以看出来,对于重载后的运算符,o1 + 1等价于o1.operate+(1),但1+o1却是错误的,因为1是int类型,此时的+号不是重载后的+号。对此的解决办法是使用友元函数。
friend Vec2D operator*(double lamda, Vec2D o); //声明
Vec2D operator*(double lamda, Vec2D o){ //定义
return o.multiply(lamda);
}
友元操作符函数只能处理1+o1这类,不能处理o1+1,因此还是需要定义普通的运算符函数。
在重载数组下标运算符时,发现其不能作为左值使用v1[0] = 3; //错误
解决方法是在数组下标运算符函数的返回值声明时加上引用。
double& Vec2D::operator[](const int index){
return index == 0 ? x : y;
}
一元重载运算符没有参数,另外如果operator@是类的友元函数(@代表单目运算符),则调用的是非重载的运算符。
前/后置 | 有无参数 | 定义方式 |
---|---|---|
前置自增 | 无参数 | T& T::operator++() |
后置自增 | 需要一个dummy参数 | T T::operator++(int dummy) |
Vec2D& Vec2D::operator++(){ //前置自增
x++; y++;
return *this;
}
Vec2D Vec2D::operator++(int dummy){ //后置自增
Vec2D temp{*this};
x++; y++;
return temp;
}
dummy只用来表示后置,并不会被传参。
流操作运算符<<,>>的第一个参数是流类的实例,如cout就是ostream的一个实例。运算符重载为类成员函数后,当调用该运算符时,左操作数必须是该类的实例,即只能使用v1 << cout;
解决方法依旧是友元函数。
friend ostream& operator<<(ostream& stream, const Vec2D& v);//cout(os)<<v1(v),即将v输出到os里
friend istream& operator>>(istream& stream, Vec2D& v);
ostream& operator<<(ostream& os, const Vec2D& v){
os << "(" << v.x << "," << v.y << ")";
return os; //返回值是ostream类型用于连续使用<<
}
istream& operator>>(istream& is, Vec2D& v){ //v必须用引用类型,否则输入到的v只是一个形参,无法修改实参。
is >> v.x >> v.y;
return is;
}
一般情况下,如果拷贝制造函数需要深拷贝,那么赋值运算符需要重载。
Vec2D类以及运算符重载.cpp
异常处理
int n1,n2;
cin >> n1 >> n2;
try{
if(n2 == 0)
throw n1;
cout << n1/n2 << endl;
} catch(int e) {
cout << e << "cannot be divided by zero" << endl;
}
throw一个异常后,不执行throw后面的语句,直接跳到catch语句。
异常处理机制的优点:可将异常信息从被调函数带回主调函数。
int divide(int a, int b){
if(b == 0)
throw a;
return a/b;
}
int main(){
try {
int x = divide(1,0);
} catch(int) { //省略参数
cout << "divisor is zero";
}
}
注意到catch语句只捕获抛出的类型,因此参数可省略。但要了解更多错误信息的话,可以传参
C++中所有异常类的基类是exception,需要包含<exception>头文件。
try{
new int[10000000000] //new一个超出内存的空间,抛出bad_alloc类型的异常
} catch(bad_alloc& e) { //bad_alloc是exception基类的一个派生类
cout << "Exeption:" << e.what() << endl; //what是exception类的一个虚函数,返回一个包含异常信息的char*
}
注意:如果throw的是派生类对象,即使catch的参数类型是其基类,也可以捕获到异常。
try{
throw Derived(); //Derived是Base的派生类,这里抛出一个Derived的匿名对象
} catch(Base& e) {
cout << e.what() << endl; //检查抛出的派生类的异常
}
因此,捕获异常的正确次序是:派生类的catch在前,基类的catch在后。
模板
由函数模板实例化的函数叫实例函数,由类模板实例化的类叫实例类。
函数模板
模板前缀:
template <typename T>
一个模板参数在一个函数中只能表示一个类型。
template <typename T>
T add(T x, T y){
return x+y;
}
cout << add(1,2.0) << endl; //会报错,函数的两个参数必须类型相同
解决方法是声明多个模板参数。
template <typename T, typename S>
函数模板本身不是类型、函数,编译器遇到模板定义时,不立即生成代码,确认模板实参后才实例化。
- 显示实例化
强制某些函数实例化,出现在模板定义后的任何位置。
//编译器生成代码void f(double)
template void f<double>(double)
template void f<>(double)
template void f(double)
- 隐式实例化
编译器通过函数调用推断模板实参。
//实例化并调用f<double>(double)
f<double>(1);
f<>(double);
f(double);
类模板
类模板函数定义时,每个函数前都要加上模板前缀 T Classname<T>::func() {...}
类模板参数也可以有默认类型。 template typename<T = int>
注意函数模板不能用默认类型。
- 非类型参数
template <typename T, int a>
例如array类就是一个类模板,声明时array<int,10>,10就是一个非类型参数。
标准模板库STL
STL的三个组成 |
---|
容器(Containers) |
迭代器(Iterators) |
算法(Algorithms) |
容器
容器用于保存一组数据,数据个体被称为元素。
容器分类 | 解释 | 如 |
---|---|---|
顺序容器 | 线性数据结构,多个元素的有序集合 | vector,list,deque(双向队列) |
关联容器 | 可快速定位元素的非线性数据结构 | set(集合),map |
容器适配器 | 必须依赖一级容器进行构建 | stack,queue |
一级容器包含顺序容器和关联容器。
STL容器 | 特点 |
---|---|
vector | |
deque | |
list | |
set | 快速查询元素,无重复关键字 |
multiset | 有重复关键字 |
map | 键值对映射,无重复 |
multimap | 有重复 |
stack | |
queue | |
priority_queue | 高优先级元素先出队 |
- 所有容器的共同函数
函数 | 解释 |
---|---|
无参构造函数 | |
有参构造函数 | |
拷贝构造函数 | |
析构函数 | |
empty() | 若容器无元素返回true |
size() | 返回容器中的元素数目 |
operator= | |
关系运算符(< != 等) |
- 一级容器的通用函数
函数 | 解释 |
---|---|
c1.swap(c2) | 交换c1和c2 |
c.max_size() | 返回该容器可以容纳的最大元素数量 |
c.clear() | 删除容器中的所有元素 |
c.begin() | 返回容器首元素的迭代器 |
c.end() | 返回容器尾元素之后位置的迭代器 |
c.rbegin() | 返回容器尾元素的迭代器,配合rend用于逆序遍历 |
c.rend() | 返回容器首元素之前位置的迭代器 |
c.eraser(beg,end) | 删除[beg,end)之间的元素,beg和end都是迭代器 |
顺序容器包含vector,queue,deque,list.
- 顺序容器的通用函数
函数 | 解释 |
---|---|
assign(n,elem) | 将n个elem赋值到当前容器 |
assign(beg,end) | 将[beg,end)间的元素赋值到当前容器 |
push_back(elem) | 将elem添加到容器 |
pop_back() | 删除容器尾元素 |
front() | 返回容器首元素 |
back() | 返回容器尾元素 |
insert(pos,elem) | 将elem插入到pos位置,pos是一个迭代器 |
迭代器
迭代器是一种泛型指针,用于遍历容器中的元素,函数都有自己专属的迭代器。只有一级容器支持迭代器。
实现:将*(解引用)、->、++等指针相关操作进行重载的类模板。
vector<int> v1; //定义一个存储int的vector v1
v1.push_back(1); //存储数据
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
vector<int>::iterator p1; //声明一个存储int型数据的vector容器的迭代器p1
for(p1 = v1.begin(); p1 != v1.end(); p++) //输出v1的元素
cout << *p << endl;
算法
算法用于操作容器中的数据。