C++ Textbook 下

Posted by Bryan on July 8, 2017

以下部分高能预警,请细心体会(本书第8~12章)今天看完前10章

8.类和队象

  • 任何对象都应该有两个要素:属性(静态特征)/行为(动态特征),由数据和函数两部分组成
  • 对象在设计时,考虑封装特性,将外部行为和内部行为分隔开,同时将各个对象之间的相对独立
  • 对象是具体存在的,类是对象的抽象,对象是类的特例
  • 多态指:由继承而产生的相关不同的类,其对象对同一消息会作不同反应

程序=对象s+消息

类和结构体的区别

  • 首先你可以将类声明时使用的class关键字换成struct,这是完全成立的
  • struct未做声明时,默认成员为public这是和class不同的地方
  • 当类的某成员函数大于3行时,可以将声明放在类内,定义移到类外,使整个类看起来更加清楚
  • 类的对象的存储空间:共有函数存储空间,自身的存储空间存储成员变量
    class Time
    {public:
     int hour;
     int minute;
     private:
     int show_times();
    };//这些放在time.h头文件中
    int Time::show_times()
    {}
    //这些放在time.cpp源文件中
    Time t, *p;
    p=&t;
    cout<<p->hour;
    cout<<t.minute;
    cout<<endl;//这些放在main中
    

    9.类和对象的进一步讨论

9.1 构造函数

  • 用户根据初始化的要求设计函数体和函数参数,这就是构造函数
  • 构造函数与类同名,在声明对象时自动执行构造函数,构造函数没有返回类型,因而也没有类型 构造函数名(类型1形参1, 类型2形参2)

    9.1.1 用参数初始化表对数据成员初始化

    class Box{Box(int,int, int);
    int height;
    int width;
    int length;}
    Box::Box(int h,int w, int len):height(h),width(w),length(len){}
    

    构造函数重载

    以下声明两个Box()

    class Box{Box(int,int, int);
    Box();
    int height;
    int width;
    int length;}
    Box::Box()
    {
     height = 10;
     width = 10;
     length = 10;
    }
    Box::Box(int h,int w, int len):height(h),width(w),length(len){}
    

    构造函数的默认参数

  • Box(int h=10,int w=10, int len=10); 这是声明,在类内部,具体定义在类外边 这相当于好多个重载函数的作用

说明

  • 调用构造函数时,不必给出实参的构造函数称为默认构造函数,或叫缺省构造函数
  • 构造对象的写法为Box box2; 不是 Box box2();这是声明函数的返回值为类Box
  • 构造函数的重载在函数声明的时候就要确定,具体定义的时候没有默认参数值,和普通的默认参数函数声明定义相同
  • 一个类只能同时有一个构造函数有效:Box();Box(int=10,int =10, int =10);同时存在二者是错误的

9.2 析构函数

  • static局部对象在main结束后才被析构;用new运算符动态建立的对象,当用delete运算符释放该对象是,先调用该对象的析构函数
  • 析构函数的作用不是删除对象,而是撤销对象占用的内存之前完成一些清理工作,使这部分内存可以分配给新对象使用
  • 析构函数不能重载,你想重载啥!
  • 析构函数也可写一些最后想说的话,在对象即将销毁之前

    9.3 对象指针

类的成员变量
int *p1;
p1 = &t1.hour;
cout<< *p1<<endl;
普通函数
void (*p)(int, int);//指向void型函数的指针变量
p = fun;
(*p)(int a,int b)//调用fun函数
类的成员函数
void (Time::*p2)();
p2=Time::get_time;
(t1.*p2)();//调用t1中的p2所指函数
  • 一个类多个对象,成员函数对应相同的代码段,以上赋值指针变量p2的应该是公用的函数代码段的入口地址 以上两句定义和指定指针指向合并为
    void (Time::*p2)()=Time::get_time;
    
  • 函数的指针变量的类型必须与赋值号右侧函数类型在以下3个方面匹配:
  • 1.函数参数的类型和参数个数 2.函数返回值的类型 3.所属的类

9.4 this指针

  • 如果一个类对应多个对象,存放多个对象的数据成员,但是不同的对象都调用同一个函数代码段
  • 不同对象的成员函数引用数据成员时,保证引用的是所指定的对象的数据成员
  • 每个成员函数都包含一个特殊的指针,成为this,它指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址
    对象指针
    Time *p;
    Time t1;
    p=&t1;
    
  • 用new运算符动态分配内存后返回一个指向新对象的指针的值,就是所分配的内存空间的起始地址
    Box *pt;
    pt=new Box;
    
  • 以上方法新建了一个对象可以使用以下方法来访问
    pt->height;
    

    合成一句话并初始化

  • Box *pt = new Box(12,15,18); //构造函数

对象的赋值

对象名1 = 对象名2

box1 = box2;

对象的复制

类型 对象2(对象1)

Box box2(box1);

Box box2 = box1;

9.5 类的静态成员

  • 静态成员变量不隶属于任何一个对象,它是类的成员,即使不定义对象,静态数据成员也被分配空间,也可被引用
  • 静态成员变量不随对象的撤销而释放,它是类的成员。
  • 静态成员变量只能在类外进行初始化
    int Box::height = 10;//height is static variable of class Box
    
  • 初始化(构造函数)中不能操作静态变量
    Box b = new Box();
    b.height;//可以使用对象名访问静态成员
    

    类的静态函数

  • 静态函数存在的价值主要是操作静态变量
  • 静态函数是类的而不是对象的成员,所以它没有this指针,所以无法访问本类中的非静态成员
  • 因为

int Box::volume()
{
	return (height\*width\*length);
}

change to the following

int Box::volume(Box *this)
{
	return (this->height\*this->width\*this->length);
}
  • 由于静态变量没有this指针,找不到内存中当前需要操作的成员变量
  • Therefore, 保证静态函数只操作静态变量,当然也可以操作非静态,只有在对静态变量时,才显得最简介易懂

9.6 友元

  • 不属于类,但是放在在类的声明,不就像是一个不是家人的friend正在家里开party吗~
  • 友元存在的价值在于虽可以不属于本类,但作为其友元,可以访问其的私有成员变量;友元函数实现多个类的数据共享;
  • 下面的程序展示了Date中的友元函数由Time中的成员函数担任,使之能访问两个类的成员,注意访问的方式
class Date;//对Date类的提前引用声明
class Time
{
public: Time(int, int, int);
		void display(Date &);
private:int hour;
		int minute;
		int sec;
};
Time::Time(int a, int b, int c)
{
	hour = a;
	minute = b;
	sec = c;
}

class Date
{
public:Date(int, int, int);
	  friend void Time::display(Date &);
private:int month;
		int day;
		int year;
};
Date::Date(int a, int b, int c)
{
	month = a;
	day = b;
	year = c;
}

void Time::display(Date &d)//对Date对象的引用
{
	cout << d.day << ":" << d.month << ":" << d.year << endl;//访问私有成员
	cout << hour << ":" << minute << ":" << sec << endl;
}

int main()
{
	Time t1(10, 13, 56);
	Date d1(12, 25, 2004);
	t1.display(d1);
	return 0;
}
  • 下面的程序,友元函数不是类的成员函数,通过重载访问两个类的私有数据
class Time
{
public: Time(int, int, int);
		friend void display(Time &);
private:int hour;
		int minute;
		int sec;
};
Time::Time(int a, int b, int c)
{
	hour = a;
	minute = b;
	sec = c;
}
class Date
{
public:Date(int, int, int);
	  friend void display(Date &);
private:int month;
		int day;
		int year;
};
Date::Date(int a, int b, int c)
{
	month = a;
	day = b;
	year = c;
}
void display(Date & d)
{
	cout << d.day << "/" << d.month << "/" << d.year << endl;
}
void display(Time & t)
{
	cout << t.hour << "/" << t.minute << "/" << t.sec << endl;
}
int main()
{
	Time t1(10, 13, 56);
	Date d1(12, 25, 2004);
	display(d1);
	display(t1);
	return 0;
}

##10.运算符重载

10.1 重载的方法

  • 函数类型 operator 运算符名称 (形参表列)
  • {对运算符的重载处理} e.g. int operator + (int a, int b)
class Complex
{public:Complex(){real=0;img=0;}
Comlex(double a, double b){real=a;img=b}
Complex operator + (Complex &);
private:double real;
double img;};

Complex Complex::operator + (Complex &c2)
{return Complex(c2.real+this->real,c2.img+img);// 直接返回一个无名对象
}
int main()
{Complex c1(1.2,3.4);
Complex c2(13.5,3.4);
Complex c = c1+c2;//c = c1.operator+(c2)
}

不能重载的运算符有5个:

  • . .* :: sizeof ?:

重载不改变运算符优先级,重载运算符参数必须至少包含一个用户自定义的类或struct,否则就失去了重载运算符的意义,而且发生错误。 一般情况:双目运算符重载为类的友元函数,单目运算符重载为类的成员函数 以上代码段修改为friend表示为

class Complex
{public:Complex(){real=0;img=0;}
Comlex(double a, double b){real=a;img=b}
friend Complex operator + (Complex &, Complex &);
private:double real;
double img;};

Complex Complex::operator + (Complex c1,Complex &c2)
{return Complex(c2.real+c1.real,c2.img+c1.img);// 直接返回一个无名对象
}
int main()
{Complex c1(1.2,3.4);
Complex c2(13.5,3.4);
Complex c = c1+c2;//c = c1.operator+(c2)
}

重载流运算符

istream & operator >> (istream &, 自定义类 &);
ostream & operator << (ostream &, 自定义类 &);

重载流运算符函数必须作为友元函数不能作为类的成员函数,原因显而易见,位置已经被stream &占据

class Complex
{public:Complex(){real=0;img=0;}
Comlex(double a, double b){real=a;img=b}
friend ostream operator << (ostream &, Complex &);
friend istream operator >> (istream &, Complex &);
private:double real;
double img;};

friend ostream operator << (ostream & os, Complex & c)
{return os<<c.real<<" + "<<c.img<<"i"<<endl;
}
friend istream operator >> (istream & is, Complex & c)
{return is>>c.real>>c.img;
}
int main()
{Complex c1;
Complex c2;
cin>>c2>>c1;//手动输入4个数字
cout<<c1<<c2;//(cout<<c1)return ostream所以可以继续<<c2
}

构造函数as强制类型转换

构造函数复习(revise) 默认构造函数

  • Complex()
  • Complex(double, double);
  • Complex(Comlex &);//e.g.在类成员函数的声明体中声明一个表示自身的对象 Complex temp(*this),最后return temp,将本身返回

Complex(double r){real=r;img=0;}

Complex(2.5) 比较 int(2.5)

  • 可以认为二者都是进行了强制类型转换
  • c=c1+int(2.5) false; c=c1+Complex(2.5) true

类型转换函数进行类型转换

类型转换函数作用是将一个类对象转换成另一个类型的数据,在Complex类中定义类型转换函数:

operator double()
 {return real;}
  • 抽象一般形式为 operator 类型名() {实现转换的语句} 在函数名前面不能指定函数类型,而且函数没有参数

11.继承与派生

C++继承可以增加代码可重用性,继承主要学会使用protected

一个派生类有两个或多个基类的称为多重继承

class 派生类名:继承方式 基类名 {派生类新增加的成员};

构建一个派生类包括以下3部分工作:

  1. 从基类接收其全部成员
  2. 调整从基类接收的成员:声明一个参数和函数名和函数返回值类型都相同的函数覆盖原来基类的函数
  3. 声明派生类时增加成员

11.1成员的访问属性

基类和派生类的访问属性

公有继承/私有继承/保护继承,基类的私有成员仍然是私有的,公有和保护成员近似变为继承的类型

不能通过派生类对象引用从私有基类继承的任何成员,只能通过派生类成员函数引用私有基类的公有和保护成员。

继承是对基类公有和保护成员的继承,基类的私有成员只能被基类的成员函数调用。

  • 保护成员相当于保险箱,任何外人均不得窥视,只有子女(派生类)才能打开

在派生类中有4中不同的访问属性:

  • 公用的:派生类内外都可以访问
  • 受保护的:对于本类同私有的,其下一层的派生类可以访问
  • 私有的:派生类内可以访问,外不可
  • 不可访问的:派生类内和外都无法访问 总结 保护(protected)是专门为派生设计的访问权限,基类的保护成员对于本类是私有的,对于保护继承的派生类可以类内访问,不可类外调用(派生类对象访问),这是因为它仍然是‘保护’访问权限,对于本类(派生类)是私有的。

在实际使用中,常用的是公有继承

11.2 派生类的构造函数和析构函数

基类的构造函数是不能继承的,所以要初始化派生类对象应该同时考虑基类的对象,方法是执行派生类构造函数时,调用基类的构造函数。e.g.

Student(int n,string nam,char s){}//基类构造函数,放在基类中
Student_derivedclass(int n,string name, char s, int a, string ad):Student(n,nam,s){}
//本句放在派生类中作为派生类的构造函数

11.3 有子对象的派生类的构造函数

类定义中含有类的对象,就像结构体中含有结构体

  • 子对象的初始化是在建立派生类时通过调用派生类构造函数实现的 定义派生类构造函数的一般形式为 派生类构造函数名(总参数列表):基类构造函数名(参数表列),子对象名(参数表列) {派生类中新增数据成员初始化语句} 下面是一个例子,monitor是子对象,是基类的对象
class Student
{public:
 Student(int n, string nam)
 {num = n;
  name=nam;}
 protected:
 int num; string name;
};

class Student1: public Student
{public:
 Student1(int n, string nam, int n1, string nam1, int a, string ad):Student(n,nam),monitor(n1,nam1)
{age = a;
 addr = ad;}//构造函数
private:
 Student monitor;//派生类子对象
 int age;
 string addr;

11.4 虚基类

C++规定:当一个derived class object(派生类对象)通过使用一个pointer to a base class with a non-virtual destructor(指向带有非虚拟析构函数的基类的指针)被删除,则结果是未定义的。

如果一个class(类)不包含virtual functions(虚拟函数),这经常预示不打算将它作为多态的base class(基类)使用。

多态导致:在一个32-bit架构中,它们将从64 bits(相当于两个ints)长到96 bits(两个ints加上vptr(virtual ptr))。

如果class(类)有一个pure virtual functions(纯虚拟函数),所以它就是抽象的。

总结:

  • polymorphic base classes(多态基类)应该声明virtual destructor(虚拟析构函数),如果一个 class(类)有任何virtual functions(虚拟函数),它就应该有一个virtual destructor(虚拟析构函数)。
  • 不是设计用来作为base classes(基类)或不是设计用于polymorphically(多态)的classes(类)就不应该声明 virtual destructor(虚拟析构函数)。

coursera C++ 笔记知识点梳理

类的内联成员函数

  1. 定义放在类体内部
  2. 声明为inline,定义放在类外面

声明一个类也就是声明了一种类型/一个作用域

  1. vector obj-a; ClassA obj-a-array[10]
  2. A::member_func(){定义}

构造函数

类在实例化对象时,必然调用了构造函数(普通构造函数,复制构造函数,类型转换构造函数[只接受一个参数,调用形如 ClassA obj-a; a=9])

class Test{
  public:
    Test(int n){}        //(1)
    Test(int n, int m){} //(2)
    Test(){}             //(3)
};
Test array1[3]={1, Test(1,2)}; //(1)(2)(3)初始化
Test array2[3]={Test(2,3), Test(1,2), 1};     //(2)(2)(3)初始化
Test *parray[3]={new Test(4), new Test(1,2)}; //Note: (1)(2)初始化

copy constructor(复制构造函数)

类默认有无参构造函数和复制构造函数

第一种情况

class Complex{double *real, *image};
Complex c1;
Complex c2(c1);//调用复制构造函数,声明形式为[classname][objname](const & objname)

复制构造函数在对象声明的时候被调用

第二种情况

void Func(A a1){} //为了减少构造函数复制带来的影响,可以将该代码改为:void Func(const A& a1){}
int main(){
  A a2;
  Func(a2); // 调用复制构造函数,a2为将变成复制构造函数的传进去的参数。为了减少复制的影响,可以改为:Func(std::move(a2))
  return 0;}

第三种情况

A Func(){
  A b(4);
  return b;//这将b作为return值的复制构造函数的参数,所以返回值取决与类A的复制构造函数的写法
}
int main(){
  cout<<Func().v<<endl;
  return 0;

析构函数

类成员变量含有指针时,要在类的析构函数中加入delete []p/p 的语句

new出来的动态指针,必须显示delete,调用析构函数。

封闭类

封闭类是包含成员对象的类,它的初始化函数可能比较重要,如果其中一个成员对象对应的类的构造函数不是默认的构造函数,则需要在封闭类中显示说明如何初始化该成员对象。

多态和虚函数(发生在运行期间)

构造函数和静态函数不能声明为虚函数,virtual只能在类的{}声明函数时使用,在类外定义该函数时,不能加virtual。

  1. 表现一:通过基类指针实现。派生类的指针可以赋值给基类指针,通过基类指针调用基类和派生类里的同名虚函数:

若该指针指向一个基类的对象,那么被调用的是基类的虚函数;若指向一个派生类的对象,那么调用的是派生类的虚函数。

  1. 表现二:通过基类引用实现。派生类的对象可以赋值给基类引用,通过基类引用调用基类和派生类的同名虚函数:

若该引用引用一个基类的对象,那么被调用的是基类的虚函数;若引用的是一个派生类的对象,那么调用的是派生类的虚函数。

Note:用基类指针数组存储派生类对象的指针,然后便利该数组,就可以对各个派生类对象做操作,是很常用的方法。也就是基类成员虚函数传入的参数或者操作的是基类自身的指针,在调用时换成派生类对象的指针就可以换成调用派生类对象的同名函数。

lambda表达式(是函数也是对象)

[] (...) -> T { ... }

Lambda表达式把函数看作对象。Lambda表达式可以像对象一样使用,比如可以将它们赋给变量和作为参数传递,还可以像函数一样对其求值。

如果lambda是一个函数,则函数名对应的是一对空的方括号,即捕获表达式。lambda 表达式的其余部分与常规 C 或 C++ 函数主体类似。

值捕获:

void scan( int* a, int length, std::function<void(int)> process)
{
  for(int i=0; i<length; i++) {
    process(a[i]);
  }
}
  int threshold = 5;
  scan(a, 10,
    [threshold](int v)
    { if (v>threshold) { printf("%i ", v); } });

抽象类

含有纯虚函数的类为抽象类,抽象类不能实例化。派生抽象类必须实现所有的纯虚函数才能定义派生对象。

输入输出

  1. 输出重定向

freopen是被包含于C标准库头文件stdio.h中的一个函数,用于重定向输入输出流。

  • freopen(“test.txt”,”w”,stdout);将std::cout的输出结果放入文件test.txt中。
  • freopen(“test.txt”.”r”,stdin);将std::cin的输入结果放入文件test.txt中。例如
test.txt:
3.14 259
--------
double f; int i;
cin>>f>>i;
  1. istream member function

istream & getline(char * buf, int size) // 读取size-1个长度的字符或者遇到\n停止,将除了\n(如果有)的其余部分复制到buf(内存缓冲区)中,同时将\n(如果有)从输入流中取走。由于会自动地在buf后面自动加上\0,所以是读取了size-1个字符。

istream & getline(char * buf, int size, char delim) // 读取size-1个长度的字符或者遇到\n或者delim停止,将除了\n或者delim(如果有)的其余部分复制到buf中。

STL简介

数据结构(链表/数组/二叉树)和算法都写成模板,使程序可以处理各种对象。标准模板库就是常用数据结构和算法的模板集合。

  • 容器:是类模板,可容纳各种数据类型的通用数据结构

  • 迭代器:类似指针,用于依次存取容器中元素

  • 算法:用来操作容器中元素的函数模板

一个数组可以看成容器:

int array[100] //sort(array,array+90); 其中int* 看成是迭代器

STL之容器概述

  • 顺序容器:vector/deque(双向队列)/list(双向列表):插入的位置同元素的值无关,你放在哪里,它就在哪里
  • 关联容器:set/multiset/map/multimap
  • 容器适配器:stack(栈)/queue(队列)/priority_queue

Note:对象插入容器时,被插入的是对象的一个复制品,许多算法要求对容器中的对象就行排序/查找,有的容器本身就是排序的,所以放入容器对象所属的类,往往还应该重载==<运算符号。

顺序容器

  1. vector 头文件

动态数组,在内存中是连续存放的,在尾部增删操作具有较好性能,因为vector预先分配内存。

  1. deque 头文件

双向队列,元素在内存连续存放。在头尾部增删元素具有较好性能,因为deque预先在两头分配内存。

  1. list 头文件

双向链表,元素在内存不连续存放,在任何位置增删元素都在常数时间内完成(默认已经找到了要增删的位置)。不支持随机存取,必须从头往后找。

关联容器

元素是排序的,插入元素要按照相应的排序规则确定其位置,在查找时候具有很好的性能。

  1. set/multiset 头文件

集合,set中不允许有重复元素,multiset允许有重复元素

  1. map/multimap 头文件<map>

map中仅有两个成员变量,一个叫first,一个叫second,map根据first的值按照某种规律(可以自定义)排序,可以快速检索元素。有点类似python中的字典,first就是key,map中不运行有相同的key,multimap允许。

  1. map and unordered_map

There is a default order for std::map based on key, however, there is no order in std::unordered_map as you can see from its name.
The complexity of std::map is O(log(n)) based on red-black tree (like a balanced tree, so log(n) is the research result) and for std::unordered_map is O(1) based on hash function.
Due to the internal theory of std::map and std::unordered_map, how to achieve these data structue when their keys are user-defined class?

template<
    class Key,
    class Val,
    class Hash = std::hash<Key>,
    class KeyEqual = std::equal_to<Key>,
    class Allocator = std::allocator< std::pair<const Key, Val> >
> class unordered_map;
// Create an unordered_map with given KeyType, 
// ValueType and hash function defined by 
// MyHashType
unordered_map<KeyType, ValueType, MyHashType> um;

Here MyHashFunction is class or struct that must contain an operator function ().
We must also implement operator == in our own class which is used for handling collisions.

template<
    class Key,
    class T,
    class Compare = std::less<Key>,
    class Allocator = std::allocator<std::pair<const Key, T> >
> class map;

By default std::map uses “operator <” as sorting criteria for keys. For default data types like int and std::string etc, operator < is available by default but for User defined classes operator < is not available by default.

Note: 顺序容器和关联容器中都有的成员函数

begin return第一个元素的迭代器
end   return最后一个元素后面位置的迭代器
rbegin return最后一个元素的迭代器
rend   return指向容器第一个元素前面的迭代器
erase  从容器中删除几个元素
clear   从容器中删除所有元素

Note: 顺序容器中常用的成员函数

front 返回容器中第一个元素的引用
back 返回容器中最后一个元素的引用
push_back 在容器末尾增加新元素
pop_back 删除容器末尾的元素
erase 删除容器中指定元素,返回被删除元素后面的那个元素的迭代器。

容器适配器

  1. stack 头文件 后进先出

  2. queue 头文件 先进先出,只能访问对头元素

  3. priority_queue 头文件 优先级对列

迭代器

分为两种,const和非const。用法

容器类名::iterator // vector::iterator ite;

容器类名::const_iterator //vector::const_iterator ite; 通过ite只能访问,不能修改元素

vector<int> v;
for(auto r = v.begin(); r != v.end(); r++)//正向迭代器
for(auto r = v.rbegin(); r != v.rend(); r++)//反向迭代器
r[10]  // 值为r后面第10个元素的引用
r<r1 //如果为真则r在r1前面