modernC++笔记

寒假学习了中国大学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;

算法

算法用于操作容器中的数据。


  1. C语言中对象表示一块内存 ↩︎

  2. 这里均是windows系统的写法 ↩︎

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值