2018.7.5

上午

一、内存

1、数据区域:

1)栈区(局部变量)

2)静态区(static、全局变量)

3)动态区(heap、堆区)

2、函数区域(代码区域)

 

二、this指针

1、位置:每个类的非静态成员函数的第一形参表上(被隐藏)

2、连接数据域和函数域

3、常指针(共4种指针:

1)指向常量的指针

2)常指针

3)指向常亮的常指针

4)普通指针)

 

三、(拷贝)构造函数(不写系统将自动生成)

1、初始化列表(程序员无法指定初始化次序,而按定义次序决定)

2、调用:

1)当一个函数的形参是一个类的对象时,必然启动拷贝构造函数。

2)当任一函数返回一个对象时,必然启动拷贝构造函数。

3、写与否的差距:

1)系统补写的构造函数无内容;

2)拷贝构造函数按位拷贝(当含有指针时,可能产生逻辑错误)

 

(*对齐原则(对齐字节):导致产生垃圾字节,按位比较时可能出错。)

 

 

 

下午

一、七种类间关系:

1、组合

类中的成员是另一个类的对象;“有一个”;

2、聚集

1)类的成员中含有某类的指针或引用,又称“所有权远程”

2)浅拷贝:实现对象间数据元素的意义对应复制。

3)深拷贝:当被复制的对象的数据成员是指针或引用时,不是复制该指针成员本身,而是将指针所指的对象(区域)进行复制。对象的指针各指向自己的那份。(二者区别:数据是否共享)

3、友元;

4、继承(“是一个”);

5、内部(局部)(函数中);

6、内嵌类(类中);

7、泛化(类型参数化;模板;STL(标准模板库))。

 

(*《深度探索c++对象模型》侯杰译)

 

(*作用域:

C:

1)全局;

2)局部。

C++:

1)函数形参作用域;

2)块作用域;

3)类作用域;

4)名空间作用域(命名空间、匿名空间);

5)全局作用域。)

 

(*重载:在同一作用域中,函数名相同,形参表不同。)

2018.7.6

一、友元

1、友元函数

将一个普通函数安插到类中,成为该类的“朋友”。于是这个函数就拥有了访问类中所有成员的特权。

2、有元成员函数

将一个类的某个成员函数安插到另一个类中,成为该类的“朋友”。于是这个函数就拥有了访问那个类中所有成员的特权。

3、有元类(“使用一个”)

将整个类安插到另外一个类中,成为该类的“朋友”。于是这个类的所有成员函数就拥有了访问该类中所有成员的特权。

4、特点:

被安插进成员的类无权访问安插进来的成员,而安插进的成员可以访问被安插的类。

5、优点:实现速度快(避免了调用类内私有成员时所需要的函数调用引起的栈调用,而是可以直接访问私有成员)

6、缺点:安全性低

7、有元关系:

1)有元关系是单向的

2)有元关系不能传递

3)有元关系不能继承

 

二、局部类:函数内定义的类

 

三、内嵌类:类内定义的类

 

四、重载运算符(运算符的函数化)

1、运算符分类

1)不可重载:

.       .*       ::      sizeoftypeid     ?:      static_cast<>

dynamic_cast<>const_cast<>reinterpret_cast<>

||    &&    ,(此三有争议)

2)可重载:

+       -        *      /       %      ^      &        |         ~      !        =      <<=    >=     >     ++     --      >><<     ==      !=      +=     -=     *=     /=      %=    ^=    &=    |=     <<=     >>=      []      ()     ->       ->*    new       new []      delete     delete [] …

2、运算符重载的形式

1)重载为类成员函数

2)重载为有元函函数

3、运算符重载的显式和隐含调用

complex a = b + c + d ; //隐含调用(更简单)

complex a = (b.operator+(c)).operator+(d) ;(拷贝构造函数)

a.operator=((b.operator+(c)).operator+(d) ; //显式调用(更能表现细节)

4、为何&&、||、,不能重载(有争议)

若重载调用的参数是一个表达式:

如,&&有短路性,当前式为真时,后式被短路,即表达式不以完成;但函数调用会先计算参数内的表达式,故两者产生矛盾。

5、()运算符重载为成员函数

1)重载形式为:f();<=>f.operator()();

2)这是为“打造一个‘函数对象’、重载的函数为其充当成员”奠定基础。

6、=运算符重载为成员函数

1)基本类型的赋值运算允许连续赋值,因此,赋值操作符应返回对同一类类型对象的引用。

2) 赋值操作符必须定义为成员函数。

3) 在这个函数中应为所有的数据成员赋值。

4) 这个函数应能察觉并处理“自赋值”的情况。(通过判断首地址是否相同)

7、三个特殊运算符

1)operator=();

2)operator&();

3)operator&() const;

4)除非需要,否则程序员不再重载这三个函数。因为它们已经将该做的事情完成得十分完善了。其中,与三大件中的其他几件不同,在继承的环境下,赋值运算符函数不会“连锁调用”。

8、operator* 和 poerator->(解引用操作符和箭头操作符)

1)用在实现智能指针的类中。

2)需要解引用操作符的const和非const版本。

3)例,下附中的代码。

9、[]运算符(下标运算符)重载为成员函数

1)重载形式:x.operator[](y); <=> x[y];

2)只能重载为成员函数,不能使用友元函数。

3)例:int A[3] <=> *(A(首地址)+3*sizeof(int)(偏移量))

10、前置++和后置++(单目运算符)重载为成员函数

1)前置单目运算符重载为成员函数,则没有形参。但后置单目运算符函数需要有一个不参与运算的整形参数,用作重载识别。

2)操作数是类对象

Clock& operator++(); //前置++

const Clock operator++(int); //后置++,int可不参与运算,但可用以识别前后置

3)前置++重载后函数应该返回更新后的引用,以供连续自加操作(++++a)。

Clock&Clock::operator++(){

++m_second;

if (60 == m_second) {

m_second = 0;

++m_minute;

if (60 == m_minute) {

m_minute = 0;

++m_hour;

if (24 == m_hour) {

m_hour = 0;

}

}

}

return *this;

}

4)后置++(不可连续自加):

const Clock Clock::operator++ ( int )

{

Clock c = *this;

++(*this);      //调用了前++:(*this).operator++()

return c;

}

5)总结(尽量使用前置++):

a、先写出前++函数,前++返回自身的引用;

b、后++直接调用前++;

c、后++返回局部对象的拷贝;

d、返回加上const是为了杜绝其成为左值。

e、前++效率高于后++。

11、元素符重载为有元函数

1)一种情况:

为Clock重载“<<”运算符,以输出对象的时间数据:

 

如果写为Clock类的成员函数:

ostream&Clock::operator<<(ostream& out)

{

out<<m_hour<<":"<<m_minute;

return out;

}

//必然导致这样使用它:

int main()

{

Clock c1(12,30);

c1<<cout;       // 这符合习惯吗?

return 0;

}

 

改写成有元函数:

friend ostream& operator<<(ostream& out, const Clock& clock);

ostream& operator<<(ostream& out, const Clock& clock)//返回ostream的引用(效率最高)是为了以供连续输出操作

{

out<<clock.m_hour<<":"<<clock.m_minute;

return out;

}

Clock c1(13,46);

cout<<c1;   // 这就符合习惯了。

2)设计为友元的另一场合:

如果一个运算符的使用,有可能是由某个基本类型数据发起调用,它就必须定义成全局的友元函数。(如,重载复数的加法:当重载为成员时,2+a为非法,所以需要设计成友元)更一般的情况,如果需要重载一个运算符,使之能够在不依靠“对象.成员函数”的形式被调用,就应该将此重载为该类的友元函数。

12、重载时不要改变其功能

13、总结

1)赋值(=)、下标([])、函数调用(())和指针对成员访问(->)等操作符必须定义为成员函数,将这些操作符定义为非成员函数将在编译时标记为错误。

2)像赋值一样,复合赋值操作符通常应定义为成员函数,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。

3)定义了诸如operator+算术运算符,也要定义operator+=等复合赋值运算符。原因是要保持运算语义的一致性。

4)所有的赋值运算符皆可以改变左值。为满足链式运算要求,函数应返回同类型的非常引用。(记住:编译器是自左向右扫描表达式的,但却是从右向左来处理的。)

5)改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用(间址访问),通常定义为成员函数。

6)对称的操作符,如算术操作符、关系操作符最好定义为友元函数。

7)指针运算符->最神奇也最复杂,不但重载了,甚至演化为类,作用于容器,叫作迭代子,那是后话了。

8)>>、<<操作符定义为友元函数。

9) 是否将某运算符重载,完全是程序需要,若无十足的理由不要重载。

 

 

 

(*引用(Colck&)

1)引用仅是变量的别名,而不是实实在在地定义了一个变量,因此引用本身并不占用内存,而是和目标变量共同指向目标变量的内存地址.

  2)表达式中的取地址符&不再是取变量的地址,而是用来表示该变量是引用类型的变量。

  3)定义一个引用时,必须对其初始化。

和对象(Clock)的区别?)

 

(*template<typename T>:模板,T为一个用户所设的抽象类,以供后续的实例化:

template<typename T>

class auto_ptr

{

public:

       explicit auto_ptr(T *p):pdata(p) {}

       ~auto_ptr() {delete pdata;}

       T& operator*() {return *pdata;}

       const T& operator*() const {return *pdata;}

       T* operator->() {return pdata;}

       const T* operator->() const {return pdata;}

private:

       T* pdata;

};

//模板类的实例化:

class A

{

public

       void f();

}

auto_ptr<A>p(new A);

p->f();

//p.operator->()->f();  //->为内置箭头操作符

 

(*构造函数:可以打造对象,也可以类型转换)

 

(*explicit:关闭构造函数的类型转换功能)

 

(*常的使用,是为了防止数据被恶意更改。如,重载运算符函数声明:

const complex operator+(const complex &c) const;

第一个const:使函数返回常量,因常量不能做赋值语句的左值,以防错误出现;

第二个const:使加数为常量,从而防止对加数的恶意修改;

第三个const:使被加数为常量,从而防止对被加数的恶意修改。)

 

(*前置类型声明:声明了类型,可以引用等,但不能创建对象,用以避免循环定义)

 

(*常函数:仅能是函数内的成员,不能是全局普通函数)

 

(*new:抛异常new、不抛异常new、放置new)

2018.7.7

一、继承

1、让已有类的成员被别的类获得的机制。

1)多继承:不同于Java的单继承。

2)多种继承:公有public、私有private、保护protected继承

2、内涵

1)吸收基类成员

全盘接受(除静态函数外),此工作程序员无法干预

2)改造基类成员

a、对基类成员访问权限的改变

b、对基类成员的覆盖

3)新增基类成员

新增派生类特有的成员

3、继承列表:

1)作用:

a、限定多继承时,多个父类对象在子类对象中的排列次序。

b、给出了多继承时,各个父类的成员在子类中的访问权限的变更规则。

2)语法:class 派生类名 : 继承方式1 基类名1 , 继承方式2 基类名2

3)指定基类的排列次序(子类对象中的父类子对象的排列次序)。

4)所继承的基类一定要有完整定义,基类不能只有声明。

4、继承方式

1)访问级别由高到低为:

a、私有继承private(化公为私)

b、受保护继承protected(折中)

c、公有继承public(原封不动)

2)继承方式从两个方面影响了访问权限

a、派生类的成员对基类成员的访问权限

b、派生类对象对基类成员的访问权限

5、子类对象可以直接访问父类成员。

6、保护继承时,基类的公有成员会变成子类中的保护成员;但可以用

using 类型名:: 将其回归原级别。

7、私有继承时,基类的所有成员会变成子类中的私有成员;但也可以用  using 类型名:: 将其回归原级别。

8、继承下的构造函数

1)基类的构造函数不被继承,需要重新定义派生类的构造函数,因为基类和派生类具有不同的成员结构。

2)基类的构造函数是自动被调用的,但对有参的构造函数要用初始化列表给出实参。定义构造函数时只需要对本类中新增成员进行初始化即可。

3)执行原则:先初始化基类成员,再初始化派生类成员。

4)连锁调用通过初始化列表体现。

5)调用次序:

a、首先调用基类构造函数。调用顺序按照它们被继承时在继承列表中指明的顺序(从左向右)。

b、然后调用成员对象的构造函数。调用顺序按照它们在本类中的声明顺序。

c、最后,执行派生类构造函数体中的语句。

9、多继承时,父类的初始化次序仅由继承列表决定

10、派生类拷贝构造函数的性质

1)如果派生类没有定义了自己的拷贝构造函数,则编译器将自动调用基类的缺省拷贝构造函数初始化对象的基类部分。

2)如果派生类定义了自己的拷贝构造函数,该拷贝构造函数一般应显式使用基类拷贝构造函数,以初始化对象的基类部分。

11、派生类的析构函数

1)职责:完成派生类把持的资源的释放,而不负责父类把持的资源。

2)只要调用了派生类的析构函数,编译器就会通过连锁调用启动基类的析构函数。

3)次序:先析构派生类部分,后析构基类部分,恰好和构造对象时相反。

12、小结:

1)当类关系是组合时,为作为成员的对象隐式调用构造函数,产生有名对象,此时初始化列表为构造函数传递实参。

2)当类关系是继承时,为作为子类组成部分的父类成员显式调用构造函数,产生无名对象,此时初始化列表也为构造函数传递实参。

3)为类自身的数据成员初始化之用。尤其是为常数据成员和引用型数据成员初始化时。

13、友元关系与继承:

1)基类授予友元关系,则友元只对基类有特殊访问权。

2)友元关系不能继承。

3)友元对派生类的成员没有特殊访问权限。

14、继承中的静态成员:

类的静态成员不参与继承,也不受继承方式的影响。无论从基类派生出多少个派生类,static成员只有一个实例。

在访问权限允许的条件下,子孙类们都可访问基类的静态成员。

15、类型兼容规则:

1)只有在公有继承下才会有类型兼容规则。

2)体现:

a、派生类的对象可以初始化或赋值给基类对象;

b、派生类的对象可以初始化基类的引用;

c、基类的指针可以指向派生类对象。

3)子类对象赋值给父类对象时,父类对象只能访问子类对象的父类子对象。

4)类型兼容规则不可用在对象数组上:

由于基类对象体量小于子类对象,当通过父类指针指向子类对象数组时,会产生数据的切割(如下图)。

故,不使用数组,而改用容器(STL:vector)。

16、同名隐藏规则:

1)由于C++对于各个类成员的命名、对于继承时派生类新增成员命名时没有任何限制的,所以继承时可能引发同名冲突。

2)当派生类与基类由同名成员时,派生类中的成员将屏蔽基类中的同名成员。

3)如要通过派生类对象访问基类中被屏蔽的同名成员,应该使用基类名限定符(::)。

例:

#include <iostream>

using namespace std;

class B1 {

public:

int nV;

void fun()  {cout<<"Member of B1"<<endl;}

};

class B2  {

public:

int nV;

void fun()  {cout<<"Member of B2"<<endl;}

};

class D: public B1, public B2  {   // 多继承

public:

int nV;

void fun(){cout<<"Member of D"<<endl;}  //同名成员函数

};

void main()

{   D d;

d.nV=1;

d.fun();

d.B1::nV=2;

d.B1::fun();

d.B2::nV=3;

d.B2::fun();

}

17、路径二义性(菱形结构问题)

1)当派生类(孙类)从多个基类派生(父类),而这些基类由从同一个基类(祖父类)派生,则在孙类中访问此共同基类(祖父类)中的成员产生的另一种访问不确定性。

2)解决方法:

虚拟继承:用virtual修饰声明共同的直接基类(class B1: virtual public B)(指针:vbptr)

为最远的派生类提供唯一的一份基类成员,而不产生多个重复的副本。

3)虚拟继承的实现机制

虚拟继承时,编译器会在派生类的对象中自动添加一个vbptr指针,这个vbptr指向一个由类维护的、本类的全体对象共享的vbtable(偏移量表,数组名),它是个整型数组,(记录偏离的字节数)(请类比虚函数的“虚函数表”vftable)。

这个表的第一项通常为0,设计者不公开其含义,或许是某种掩码或标志,目的为了某些设计的需要。 其规律是:当类不带虚函数时,它是0x00000000;当类带虚函数时,它是0xFFFFFFFC。

这个表的第二项为:在派生类中所含虚基类对象部分的首地址与该虚基类的vbptr之间的偏移量,是个整数。

4)虚拟继承机制的图示:

孙类对象对其成员的排列:

例:

(基类成员不止一个时,也只需一个vbptr记录首成员的偏移量)

当虚基类不是祖先类时:

5)虚继承时数据成员访问权限的服从原则:

编译器遵循“就低不就高”的原则,当两父类继承共同祖先的继承方法不同时,子类的继承权限按最低权限继承。

 

(*STL:容器、迭代器、算法、函数对象、适配器、分配器)

2018.7.8

一、多态

1、概念:多态性(polymorphism)是指具有不同功能的函数们可以使用同一个函数名,调用不同类的不同功能的函数,而且是非人工干预的。(非重载关系;类型决定行为)

2、多态性:向不同的对象发送同一个消息,个类对象在接收时会表现出不同的行为。每个对象可以用自己的方式去响应相同的消息。

3、优点:若无多态(c语言,面向过程编程),仅能通过switch-case语句判断同一语句调用哪个类的函数,极大增加程序的维护、扩展成本(新加模块时需要修改switch-case语句);多态下只需新增继承类,无需使用switch-case语句。

4、分类

1)专用多态

a、强制多态

i、const_cast<>

ii、static_cast<>

iii、reinterpret_cast<>

iv、dynamic_cast<>

v、C风格的()

b、重载多态

i、函数重载

ii、运算符重载(难点)

2)通用多态

a、参数多态

i、函数模板

ii、类模板

b、包含多态

i、虚函数

5、虚函数(重写,override)

virtual,加virtual即加指针(虚函数指针,vfptr,因其在地址首位,所以通过this指针找到);

虚函数表(vftable)存函数地址(静态区),按基类的声明次序安排地址存放次序规则,故访问时直接按次序访问表内地址,子类表遵从基类规则,即使子类函数声明次序不同于基类,仍按基类次序在表中声明。若子类新增其他虚函数,通过父类类型来访问子类新增虚函数将报错;

重写<=>重写虚函数表。

1)无论重写与否,派生类无法改变该函数已经为虚函数这一事实,故,重写虚函数时,virtual关键字可省略

2) 严格性(虚析构函数除外):派生类中虚函数的声明必须与基类中的声明方式完全匹配(函数的返回类型、函数名、形参表要完全相同,只是函数体不同)。

稍有例外:返回基类的引用(或指针)的虚函数,派生类中的虚函数可以返回派生类的引用(或指针)——协变类型。(类型兼容规则)(只有在VC6.0以上的版本才能通过编译)

3)虚函数(需要调用this指针)只能用于非静态、非友元、非内联(均无this指针)的成员函数。

6、静态类型、动态类型

1)类型决定了操作行为;对象的类型决定了该调用哪个类的成员函数。

2)静态类型:编译时确定下来的类型,又称“表面类型”。

3)动态类型:运行时实际绑定的类型,编译时不确定,运行时才确定,又称“实际类型”。

4)虚函数绑定实际:

运行时确定虚函数调用:通过引用或指针调用虚函数时,编译器将生成相应的代码,确定在运行时该调用哪个函数,被调用的是与动态类型相对应的成员函数。

5)虚函数可以动态绑定也可静态绑定,取决于通过引用或指针调用还是对象调用。

6)例:Base父类、Derived子类

7)产生动态多态的条件:

a、类间关系满足类型兼容规则(public);

b、基类的某成员函数定义了virtual;

c、派生类中要重写该虚函数;

d、对象要用指针或引用来调用虚函数成员函数。

8)virtual的语义:

a、对基类中但凡要表现出特异性行为的函数,就要定义为virtual。

b、凡继承于含有虚函数的基类的各派生类,若无特别理由,都肩负了重写虚函数的责任。

c、virtual 对于类族,是动态联编的标志,但仅是充分条件,不是必然结果(必要条件:引用或指针的调用)。

9)函数调用的方式分类

a、按名调用:通常,函数的调用是,编译器扫描到函数调用语句,会径直到代码区按函数名找到该函数,翻译成调用指令。

b、按址调用:当指针已经指向一个函数,可以用该指针名充当函数名来调用该函数,此时指针变量存放了哪个函数的地址,就调用哪个函数。

10)虚函数的实现机制

a、动态绑定技术的实质是:将函数的“按名查找—调用”变成“按位置查找—调用”。借助一个存放函数地址的存放处,找到想要调用函数的地址,按址调用它。函数调用就成了“按下标找到函数地址,进而调用函数”。

b、虚函数的调用机制就是:抛开函数名称,完全按数组定位,取数组元素,调用之。而不管此时找到的函数名称是谁。只要把基类原来位置上的函数换成派生类的新函数——旧瓶装新酒,就可以调用到子类重写的函数。

c、由于虚函数表vftable中函数类型各异,故,虚函数vfptr指针类型为void。

d、图示:

 

 

 

 

(*内联函数inline(c中的宏):函数调用时需要产生建栈与退栈,从而产生系统开销,所以内联函数用函数体替换调用处的函数名,避免栈空间的开辟,从而也没有了this指针)

 

(*RTTI:Run-Time Type Identification)

 

(*ADA语言:有对象,但无多态语言。美国军方垄断)


CSUer