LESSON 1
1.大学学习,不但要学习基础知识,更要培养思维能力。2.两遍编译过程(two-pass compiling)
大部分编程语言采用单遍编译过程,但C和C++语言却都采用两遍编译过程:
(1)第一遍(first pass):
将源程序文件编译为汇编语言文件,汇编语言文件名称的后缀为 asm等。一般编程人员不使用这类文件。
调试程序时如果希望阅读汇编语言文件,可以在调试中将VC++的菜单项“view”下拉,
在子菜单“Debug Windows”中选择最后一项“Disassembly”(热键为Alt+8),即可看到汇编语言文件。
(2)第二遍(second pass):
将汇编语言文件编译为目标文件(object files),目标文件名称的后缀为 obj。
LESSON 2
1.系统地学习一下英语语法,不需要很深入,一定要系统的去学习。2.英语精读很有用,不要不求甚解,精读要一句一句地就理解,去翻译,读一句去理解一句,而不是走马观花
地看了很多,其实不是很理解,那样进步很慢。
精读之后然后泛读,扩大词汇,巩固语法。
3.高水平的英语学习,要有英语思维,不经过大脑即可脱口而出。
LESSON 3
1.在正常编译过程之前作为预备动作而执行,编译过程结束后不占用存储空间。2.宏和函数的区别:宏节省时间但是占用空间,函数节省空间但是增加时间。
3.头文件用<>包括起来,系统就根据这点到系统的文件夹中去调用,
用双引号""包括起来,系统就根据这点到用户程序的文件夹中去调用。
LESSON 4
1.int arr[10];arr是一个数组名,又是数组首地址,也是一个数组指针且为常量指针(int* const arr),因为
arr为常量指针,所以其不允许重新赋值,例如arr++是错误的。如果arr不为常量指针,那么这个
数组在栈中开辟出来后,就不能永远有效地使用它。
2.char *nm = "Nice";
char name[] = "OK";
"OK"在栈区,"Nice"在数据区。
3.指针数组,全是指针的一个数组,
char* name[] = {" ","Monday","Tuesday","Wednesday","Thursday", "Friday", "Saturday", "Sunday"};
数组指针,一个数组的指针,
char (*name)[];
4.函数指针
void Fun();
int main() {
void (*ptr)() = Fun;
}
函数指针数组
void (*ptr[])() = { Fun1, Fun2, Fun3 };
LESSON 5
1.引用是“别名其表,指针其实”。引用的类型必须和变量的类型一样,引用实质上是一个指针,引用完全具有指针的功能。
eg:
void main() {
int i = 4;
int j = 8;
int& r = i;
}
栈区内存地址 变量内容
0X0065FDF4 i = 4
OX0065FDF0 j = 8
0X0065FDEC i的引用r,内容为地址0X0065FDF4,也就是i的地址。
从上述的例子可以看到,引用不占用空间是错误的说法,引用实际上就是指针。
引用一经初始化后,就不能重新指向其它的变量,即所存储的地址值不能那个再次更改。
但是人们并不能拿到引用这个指针的地址,C++语法使其操作起来就像操作原来的变量一样,
语法层面将其指针的特性进行了隐藏。但是编译器对其实现时,将其处理为指针。C++这样做的
好处使用户用起指针来更加方便。
2.函数调用过程:
void Swap();
void main() {
int a;
int b;
Swap(a, b);
}
A.保存调用程序(function caller,此处即main())在调用子函数swap( )时的地址,即返主地址,
将其压入栈内,以便在调用子函数swap的操作结束之后返回原地。
B.将函数返回值以及实参复制为副本(copy)后压入栈内,以便代替形参来参加函数的运行。
C.将函数运行时将要使用的各寄存器(主要是EBP、EBX、ESI和EDI)的内容压入栈内,加以保存。现场保护。
D.执行被调用函数swap()的函数体内的全部语句。
E.如函数有返回值,则将该返回值写入寄存器EAX内,通过它传递给调用函数的程序(即main())。
F将各寄存器内容弹出栈,释放被占栈空间,恢复调用函数的程序的运行状态。回复现场。
G.按照返主地址将控制权交还给调用函数(即main())。
3.void
A.void变量是“什么变量都没有”。
B.void fun();表示函数fun不返回任何变量。
C.void指针表示“任何类型的指针”。
D.void fun(void*);表示函数fun的形参可以是任何类型的指针。
4.new与malloc相比的优点是:
A.new能够按照变量类型自动的分配该类型所需空间长度,不必使用sizeof。
B.new能够返回正确的指针类型。
C.new出来的对象会自动调用对象的构造函数,将对象初始化。
LESSON 6
1.堆区块的结构。每当调用运算符new在堆区块内分配空间时,系统就为用户分配一个堆区块。该区块包括首部域
和用户数据两个域。首部域存放管理数据,用户数据域存放用户数据。首部域的长度是固定的,
32个字节,用于存放8个管理数据,每个数据都是一个整形变量。用户数据长度是变动的,不小于
用户数据的长度,其最短字节为16字节。为了方便管理,它的长度按照16字节递增。而new返回来
的指针指向的是用户数据域名的首地址。首部域的八个管理数据包括链表首位结点的首地址,
用户数据长度,用户堆区块数量,该区块的序号等。
2.堆区块的建立和管理。
所分配的堆区块空间按照双向链表(包含前向链表和后向链表)进行管理。每当系统函数
或者用户函数调用new申请分配堆区空间时,系统即建立一个堆区块,形成新结点,
将其插入到前向链表的前端,形成前向链表的首结点。
eg:
void main() {
int* ptr1 = new int(10); // 初始化为10,并不是长度为10的数组。
double* ptr2 = new double(12.34);
int* ptr3 = new int[3];
// 读取第一个堆区块
int* show = ptr1 - 8; // 移动指针到堆区块的首部域的第一个数据的地址。
for (int j = 1; j < 9; ++j) {
cout << "Element " << j << " address : " << show;
cout << " Value : " << *show++ << endl;
}
// 读取第二块和第三块堆区块的数据。可以通过里面的首部域数据,将这三个结点连接起来。
}
3.new operator 和operator new。
http://blog.csdn.net/wudaijun/article/details/9273339
A.new operator: 指我们在C++里面通常用到的关键字。
eg:
A* a = new A;
delete a;
这个操作第一步分配内存。第二步调用A的构造函数初始化对象。第三步返回分配指针。
事实上分配内存就是有operator new(size_t)来完成的,如果类A重载了operator new,
那么将调用A::operator new(size_t),否则调用C++默认提供的operator new(size_t)。
delete a这一操作也类似,包含了调用析构函数和operator delete(size_t)两个步骤。
B.operator new: 它指一个操作符,并且可被重载。operator new就象 malloc 一样,
operator new 的职责只是分配内存,它对构造函数一无所知。
A* a = (A*)operator new(sizeof(A));
operator new有三种形式:
throwing (1)
void* operator new (std::size_t size) throw (std::bad_alloc);
nothrow (2)
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
placement (3)
void* operator new (std::size_t size, void* ptr) throw();
(1)(2)的区别仅是是否抛出异常,当分配失败时,前者会抛出bad_alloc异常,后者返回null,不会抛出异常。
它们都分配一个固定大小的连续内存。
A* a = new A; //调用throwing(1)
A* a = new(std::nothrow) A; //调用nothrow(2)
(3)是placement new,它也是对operator new的一个重载,定义于#include <new>中,它多接收一个ptr参数,
但它只是简单地返回ptr。它可以实现在ptr所指地址上构建一个对象(通过调用其构造函数),这在内存池技术上有广泛应用。
4.变量的两个基本属性:生命周期和作用域。
5.所有静态变量在使用前必须加以定义,它们被定义时即自动被初始化,具有缺省值为零,
这一点与全局变量(外部变量)相同。因此,对于int型静态变量用0值初始化是多余的。
LESSON 7
1.本课程中所有的地址都是虚拟地址,即在该程序所分配到的内存中的相对地址,这并不代表内存中的真实地址(绝对地址)。
内存很大,操作系统只是拿出一小部分给程序用。
2.对象的数据存储空间分为两部分:栈区和数据区,数据区用于存放静态变量,栈区存放该对象的非
静态数据。代码区存放成员函数的代码,如果类中包含虚函数,则栈区还存放该对象的虚指针,数据区
存放该对象的虚地址。
3.类的接口部分与实现部分的分离:对用户而言,他们只关心类的接口,只要类的功能不变,也就是类的
接口不变,所以无论类的实现部分如何改变都不影响用户,也不需要修改用户程序。
用户调用类的成员函数时不须了解类的源代码(即成员函数的定义),而只须了解类的功能也即类
的接口即可。因此用户在使用时只须将已经该动过的类的实现[方法]的目标代码连接在用户程序之内即可。
软件供应商提供头文件和各隐藏文件的目标代码模块(即目标文件),以供用户用于编写各种用户程序。
4.将类接口的重要信息放在头文件中而将不希望用户了解的信息(类的实现[方法]部分)
放在不公开发表的源文件(以下称为隐藏源文件)中,这符合最低权限原则。
5.描述类的功能而不谈其实现细节的做法,有人称为数据抽象(data abstraction),
有人称之为数据隐藏。
LESSON 8
1.常量数据和数据的引用不能再构造函数中直接赋值,需要用初始化成员列表。eg:
class A {
public:
A(int a, int& b)
: pi(a)
, r(b) {
}
private:
int& r;
const int pi;
}
LESSON 9
1.对象成员变量的构造函数的调用顺序和其析构函数的调用顺序相反。2.对象成员变量的构造函数的调用顺序和类体中其声明的顺序一致,与初始化列表的顺序无关。
3.先调用对象的成员变量的构造函数,然后调用该对象的构造函数。
4.一个对象,成员变量存放于栈区,成员函数存放于代码区,这些成员函数是这个类所有对象所共有的。
试问这些成员函数怎么知道该使用哪个对象的数据成员,怎么知道各个对象的地址?原来每个对象都将
自己的地址存放在this指针之中,而每个成员函数的参数表中都隐含着这个指针,通过这个指针找到该
对象,获得该对象的数据成员。
eg:
class A {
public:
void set_b(int b1) {
b = b1;
}
private:
int b;
}
void main() {
A a;
int b1 = 10;
a.set_b(b1);
}
分析:对象a存在栈区,对象a的数据成员b也存在栈区。而函数void set_b(int b1)存在代码区,该函数
给对象a里面的成员b进行了赋值,那么从函数表面看,该函数是如何辨别当前的b是对象a的成员呢。
实际上编译器将该函数进行了加工,函数参数增加了一个对象指针,对象在调用该函数时,把自己的指针
传给了函数,函数则可以通过其找到它的成员。编译器将void set_b(int b1)加工为:
void set_b(A* a, int b1) {
a.b = b1;
}
而每个对象进行调用时a.set_b(b1);实际上的动作是set_b(this, b1);
5.由于对象的this指针不属于对象本身,因此使用sizeof宏所得的对象长度中并不包括this指针在内。
6.this指针的一个用法是允许依靠返回该类对象的引用值来连续调用该类的成员函数。
7.注意返回this指向的对象还是返回的this指向对象的引用。
eg:
Date GetDate() { // 返回的是该对象的拷贝
return *this;
}
Date& GetDate() { // 返回的是该对象的引用。
return *this;
}
8.类的静态成员变量存放于数据区,生命周期和程序一样长。其定义要在类体外面定义。
eg:
class A {
private:
static int a;
}
int A::a = 10; // 缺省初始化为0。
9.静态成员函数无法访问非静态数据。有两个原因:
第一个原因从语法逻辑上面说:是因为静态成员函数可以通过类名访问,也就是对象还没实例化
该函数已经开始访问对象成员,巧妇难为无米之炊,数据成员还没有在栈上面生成出来,如何访问?
第二个原因从根本上说,就是静态成员函数和非静态成员函数之间的区别所决定,
它们之间的最大区别就是参数里是否隐藏着一个this指针。静态成员函数没有this指针,
所以其找不到对象的成员变量,所以其不能访问对象的成员变量。
LESSON 10
1.eg:class Base {
int a;
}
class Derived : public Base {
int b;
}
void main() {
Base b;
Derived d;
// 证明基类对象在栈区里面先有基类的数据成员然后是子类的数据成员。
Derived* p = &d;
int* ptr = (int*)p;
cout << *p << endl; // 输出基类a的值。
cout << *(p++) << endl; // 输出子类b的值。
}
对象b在栈区的存储着变量int a。而对象d在栈区存储着基类部分的成员变量int a和自己的变量int b。
当建立派生类的对象后,派生类对象的内存栈区空间中既包括基类部分的也包括派生类部分的
所有非静态数据成员,一律存放在派生类对象的内存栈区空间中。
内存栈区空间内的非静态数据成员的排列顺序是基类部分在前,派生类部分在后。
如果有虚函数,则内存栈区空间内还包括虚指针VPTR,而该虚指针则指向内存数据空间内的虚函数地址表vtable。
2.派生类对象的栈区存储内容与继承方式和访问权限无关。C++的继承是加机制,私有继承不能访问基类成员,
并不代表子类没有继承基类的数据成员,只是没有访问权限罢了。所以派生类对象的栈区还是基类到子类的所有
数据成员。
LESSON 11
1.继承访问映射关系表基类成员的访问属性 派生类的继承方式 映射到派生类的访问属性(派生类内访问权限)
private public 不可访问
protected public protected
public public public
private protected 不可访问
protected protected protected
public protected protected
private private 不可访问
protected private private
public private private
2。而一个类对象的栈区存储内容则包括该类及其所有基类(直接的和间接的)的全部非静态数据成员
(无论能否访问),也包括虚指针,但不包括任何静态数据成员和成员函数,它与继承方式及访问权限无关。
3.单继承构造函数的调用顺序:先祖先(基类),再客人(成员对象),后自己(派生类本身)。
析构函数的调用顺序正好和构造函数相反。
先调用基类的成员函数时,也是按照先客人再自己的顺序。
4.多继承构造函数的调用顺序:和单继承的顺序一致,只不过同时继承的多个基类的顺序是按照声明的顺序。
eg: class Z : public X, public Y;
基类X和Y的构造函数的调用顺序决定于多继承时声明基类的顺序。与初始化列表的顺序无关。
5.对象栈区存储内容的排列顺序取决于成员变量声明的顺序。
LESSON 12
1.多继承的二义性,例如两个基类都有数据成员int a,obj.a无法规定是哪一个a,因此出现二义性。多继承的二义性有两种情况:
A.访问不同基类中的相同成员时可能出现的二义性
eg: Base1 中有a,Base2中也有a,Derived 继承Base1和Base2。
B.访问共同基类中的成员时可能出现的二义性
eg:Base中有a,Base1继承Base,Base2继承Base,Derived继承Base1和Base2。
2.虚基类:程序中在class base的派生类的继承方式前加上virtual关键词,从而声明class base为虚基类。
主要解决了多继承情况B(访问共同基类中的成员时可能出现的二义性)的二义性。
eg:
class Derived : virtual public Base; // Base则被声明为虚基类。虚基类解决了多继承的二义性问题。
eg:
class base {
public:
base(int i) { a=i; cout<<"CONS_B"<<endl; }
int a;
};
class derive1 : virtual public base {
public:
derive1(int i):base(i) { cout<<"CONS_D1"<<endl; }
};
class derive2 : virtual public base {
public:
derive2(int i):base(i) { cout<<"CONS_D2"<<endl; }
};
class grand : public derive1, public derive2 {
public:
grand (int i) : derive1(i+1), derive2(i-1), base(i) {cout<<"CONS_G"<<endl; }
};
void main()
{
grand g(123);
cout<<"g.a = "<<g.a<<endl;
}
输出:
CONS_B
CONS_D1
CONS_D2
CONS_G
g.a = 123
以上输出结果显示class base的构造函数值调用了一次而不是两次。如果不用虚基类的方式,则base的构造函数
会调用两次,也就是说不用虚基类,会产生两份成员变量a。
3.虚基类名曰“共享继承”,又名曰“各派生类的对象共享基类的的一个拷贝”。
4.虚基类部分的初始化和一般多继承中基类部分的初始化在语法上是相同的,但构造函数的调用顺序不同,
服从以下规则:
A.同一层中先调用虚基类的构造函数,然后调用非虚基类的构造函数。
B.同一层中存在多个虚基类时,按照各虚基类的声明顺序来调用。
C.如虚基类V由非虚基类B派生而来,则先调用基类B的构造函数,然后调用派生类V的构造函数。
例子详见课件chapter4。
5.向基类传递参数的规则:
(第一条)在没有虚基类的情况下,只准逐级向上传递参数,而不允许越级向间接基类传递参数。
(第二条)在有虚基类的情况下,虚基类的初始化参数只能够从最下层用于建立对象的类中直接传递上来。
详细例子见课件chapter4。
6.虚基类用的并不多,语法复杂,容易出错,理解还是很容易的,可以查看chapter4的内容,原理还是有必要
了解一下,学习原理,了解C++的内部实现思路。
LESSON 13
1.多态性:能够呈现不同形态的特性和状态。2.两种多态性:
A.静态联编(static binding):也称为编译时的多态性。编译器进行编译时在函数调用指令表中找到
多个重载的函数中对应的一个函数,将它们与主程序中调用它们的代码进行联编(binding),以备主程序
运行时正确的调用。
B.动态联编(dynamic binding):“所谓的"运行时的多态性。编译器进行编译时,根据程序代码内容,按照
当时动态地确定this指针,找到相应的虚函数,将它与主程序中调用它的代码进行联编(binding),以供
主程序在运行中调用它。
3.三个over的区别:
override:重载,子类重载父类的函数。
overload:重载,函数重载,同一类级别。
overwrite:重写,子类重写父类虚函数。
LESSON 14
1.重载运算符有两种形式,一种是友元函数,另一种是成员函数。
2.重载运算符的友元函数形式。重载运算符一般的目的是作用于自定义类型的对象。例如重载加号,使得
point1 + point2。所以该重载必须可以访问类Point的成员变量,所以该重载函数为Point类的友元函数。
eg:
class Point {
public:
friend Point operator+(const Point& point1, const Point& point2) {
Point point;
point.x = point1.x + point2.x;
point.y = point1.y + point2.y;
return point;
}
private:
int x;
int y;
}
编译器如何知道何时调用重载的运算符,还是调用预定义的运算符?编译器完全是根据操作数的类型类确定。
3.通过使用友元类和重载赋值运算符实现一个类给另一个类赋值。
eg:
class A {
friend class B;
}
class B {
B& operator=(const A& a);
}
4.重载赋值运算符时,注意自我赋值的判断。
eg:
class A {
public:
A& operator=(const A& a) {
if (this == &a) {
return *this;
}
...
}
}
LESSON 15
1.成员函数形式的重载运算符的实质就是定义了一个成员函数。2.自增自减运算符重载:编译器如何区分前置和后置的自增或自减?
为了识别重载的前置和后置的运算符,每个重载的运算符函数必须有一个明确的特征,以便编译器能够确定
需要使用的重载函数。重载前置自增(自减)的方法和重载其他一元运算符相同。
例如当编译器遇到++a时,编译器就会调用成员函数a.operator++()。如果是后置的话,则重载函数加了一个
无用的参数operator++(int),当作占位符,来区分前置重载函数operator++()。
可以推测:当时C++编译器设计者,在考虑重载自增自减运算符时,很显然该重载函数应该写为operator++()。
但是改运算符可以前置可以后置,只有这一个成员函数是无法区分的。区分的最简单的办法就是将该函数
进行重载,也就是给该函数加个无用的参数,以此来重载,形成两个函数。
eg:
class A {
public:
A& operator++() { // 前置重载(先加减,再取值) 由返回值可以看出这哪个返回值可以做左值。
x++;
return *this;
}
A operator++(int) { // 后置重载(先取值,再加减)
A a = *this;
x++;
return a;
}
private:
int x;
}
3.数据类型转换:
显式转化:语法上直接将其转换。eg:int* a = (int*)p;
隐式转换:编译器默认的将其转换。eg:double j; int a; a = j;
4.在需要进行类型转换时,如果用户不对其进行显式转换,则编译器自动地对其进行隐式转换。
编译器将按照如下规则进行转换。
(A)当使用表达式E1 = E2进行赋值时,“=”右端E2的数据类型强制转换为左端的E1的类型后再进行赋值。
eg:double j; int a; a = j;
(B)当函数的实参与形参的类型不同时,实参类型被强制转为形参类型。
(C)当两个操作数的数据类型不一致时,在算术运算前,级别低的类型自动转换为级别类型高的类型。
数据类型级别排序(从低到高)
(a)char -> short[int] -> int -> unsigned[int] -> long -> unsigned long -> double
(b)float -> double
eg:a(int) + b(double) = c(double)
5.类型转换运算符函数:将一个类转换为其他自定义类型或者转换为基本类型。类的对象类型可以转换
为其他数据类型,可以完成这种类型转换的成员函数被称为类型转换运算符函数。
eg1:
class Integ {
operator int() { // 类型转换运算符,返回基本类型。
... // 用于将对象转换为它的整形数据成员。
return value; // 虽然不标出返回值,但实际上返回的是一个整形数据。
}
operator Date() { // 返回自定义类型。
...
return date;
}
}
void main {
Integ obj(1);
// 调用类型转换运算符函数的三种方式。
cout << int(obj) << endl; // 显式
cout << (int)obj << endl; // 显示
cout << obj << endl; // 隐式
cout << Date(obj) << endl;
cout << (Date)obj << endl;
}
在使用类型转换运算符函数时,要注意三个事项:
(A)类型转换运算符函数必须是类的成员函数,并且是非静态的。它不能定义为友元函数。
(B)类型转换运算符函数的定义和声明不要写返回值(只是在函数名上面不写返回值,函数体内还是要写返回值的),
该函数名称就是类型转换的目标类型,也即返回值。
(C)类型转换运算符定义时不必带有任何形参,因它是用于类型转换的。
LESSON 16
详见《多态原理》1.基类指针可以指向子类,但是子类指针不能指向基类。
也就是说“原指向小空间的指针可以指向大空间,但是只能指向它的前一部分”。
“原来指向大空间的指针不允许再指向小空间”。
然而使用虚函数后,原指向小空间的指针就可以指向所有大空间内的虚函数。
eg:
class base {
public:
void who() { cout<<"base"<<endl; }
};
class first : public base {
public:
void who() { cout<<"the first derivation"<endl; }
};
void main()
{
base obj1, *ptr;
first obj2;
ptr = &obj1;
ptr->who(); // base
ptr = &obj2;
ptr->who(); // base
}
ptr->who希望的是输出base和first。但是编译器在静态联编时就认定ptr只能指向对象中基类部分的空间,
因此只能找到并调用base::who()。也就证明了没有虚函数时,基类的指针只能指向基类部分的东西,而不能
使用子类定义的数据和函数。
2.虚析构函数:使用指向基类的指针指向派生类对象时,如果不将析构函数定义为虚函数,则其销毁时,派生类
的析构函数将被忽略。
eg:
class Base {
public:
~Base() { cout << "Base" << endl; }
}
class Derived : public Base {
public:
~Derived() { cout << "Derived" << endl; }
}
void main() {
Base* base = new Derived();
delete base; // 只调用了基类的析构函数,只输出了Base。
}
在面向接口编程时,尤其要注意这一点,因为面向接口编程时,都是用基类指针指向派生类对象的,
所以析构的时候一定要全部析构完,所以使用虚析构函数。
必须将析构函数声明为虚函数,才能在程序结束时自动地调用派生类的析构函数,
从而将派生类对象申请的空间退回给堆。
3.静态联编的机制
A. 所有成员函数(包括构造和析构函数)的函数体都位于代码区内。
B. 所有成员函数的调用指令(即入口)都位于代码区的“函数调用指令表”内。例如:
00401005 jmp ostream::operator<< (00401380)
0040100A jmp ostream::operator<< (00401440)
0040100F jmp endl (004013e0)
00401014 jmp derive::derive (00401250)
00401019 jmp base::base (004012e0)
0040101E jmp main (00401060)
00401023 jmp flush (00401490)
00401032 jmp derive::fun (00401320)
C. 编译系统进行编译时,在“函数调用指令表”内找到相应的函数调用指令,将它与主函数代码联编,
以备运行时调用。
4.所谓动态联编的机制
这归功于虚函数地址表(简称虚地址表)和虚地址表指针(简称虚指针)。在进行编译时,
编译系统对每个带虚函数的类执行以下步骤:
A. 所有成员函数(也包括所有虚函数)的函数体都位于代码区内。
B. 所有成员函数(也包括所有虚函数)的调用指令(即入口,例如jmp base::fun)
都位于代码区的“函数调用指令表”内。例如以下所示函数调用指令表:
00401005 jmp ostream::operator<< (00401670)
0040100A jmp ostream::operator<< (004017a0)
0040100F jmp derive::derive (00401310)
00401014 jmp hex (00401530)
00401019 jmp endl (00401750)
0040101E jmp derive::fun (00401370)
00401023 jmp ios::unlock (00401640)
00401028 jmp base::act (00401470)
0040102D jmp base::base (00401430)
00401032 jmp derive::act (004013d0)
00401037 jmp ios::setf (00401580)
0040103C jmp ostream::operator<< (004016d0)
00401041 jmp base::fun (004014d0)
00401046 jmp main (004010b0)
0040104B jmp ios::lock (00401610)
00401050 jmp flush (004017f0)
C.编译系统为每个类建立一个位于数据区内的该类的虚函数地址表(Virtual Function Address Table或vtable,
简称虚地址表),表中的每个单元指向位于代码区“函数调用指令表”内的相应虚函数的调用指令。
D.在建立该类的对象(对象一般位于栈区内)后,在该对象内设置一个该对象的虚地址表指针
(Virtual Function Address Table pointer,或vtable pointer,或VPTR,简称虚指针)。
该虚指针VPTR指向该类的虚地址表vtable的起始地址。
E.然后根据程序代码的变化,利用虚指针VPTR加上偏移量的形式将指针指向虚地址表vtable中相应类
(或为基类,或为派生类)的相应虚函数调用指令,将该虚函数与调用程序(calling function)联编,
以供后者调用。
5.所谓动态联编,并不是程序运行时进行联编,而是在编译时确定的。
其实是在编译时由编译系统根据程序代码的变化而确定相应虚函数的调用指令,从而实现相应虚函数
的联编。这并非在程序运行中确定,而是在编译时早就确定的。因此,这不是真正意义上的动态联编,
其实质仍是静态联编。
6.这里有指针的三层使用:第一层是使用类对象的指针(或引用)来指向类的对象
(例如ptr指向class cl的对象obj);第二层是使用类对象中的虚指针(VPTR)来找到数据区中的虚地址表vtable;
第三层是使用该类虚地址表中的虚函数调用指令的地址来找到相应的虚函数调用指令(例如jmp cl::fun),
然后即可调用虚函数(例如cl::fun( ))。
7.带虚函数的类的对象长度
A.无虚函数的类的长度只是其所有非静态数据的长度。
B.带虚函数的类的长度是其所有非静态数据的长度加上虚指针长度,但与虚函数的数量无关。这意味着无论虚函数的
数量多少都只有一个虚指针。
C.虚指针只指向一个虚地址表,即this指针所指向的那个对象的虚地址表。
8.虚指针VPTR一般位于对象内存栈区空间靠前单元处,在VC++6.0中虚指针VPTR位于内存栈区空间的第一个单元处。
9.子类有子类的Vtable,基类有基类的Vtable。但是同一个类用的是同一个Vtable(也就是同一类的各对象共享一个虚地址表)。
10.基类和派生类中如有两个以上虚函数,则各类的虚地址表中各虚函数调用指令的地址的排列顺序必须完全相同。
而此顺序决定于基类中各虚函数的声明顺序。(也就是子类和基类各自的虚函数表的函数指令排列顺序完全相同,
取决于基类中各个虚函数的声明顺序。)
11.eg:
class Rate {
public:
voie Fee();
virtual void Area();
}
class Square : public Rate {
public:
virtual void Area();
}
class Rectangle : public Rate {
public:
virtual void Area();
}
void main() {
Square sq(3, 4, 5);
cout<<sq.Fee( )<<endl;
Rectangle rec(3, 4, 5, 6);
cout<<rec.Fee( )<<endl;
}
因为this指针指向square类的对象sq,所以square::vtable中只包括square类的虚函数Area()的调用指令的地址。
联编过程如下:
A.main( )要求调用sq.Fee(),但class square中没有函数Fee( ),根据调用规则,找到基类class Rate中的函数Fee()。
B.class Rate中的函数Fee( )准备调用虚函数Area( ),而所有类中都有此虚函数,应该调用哪一个呢?
这就通过this指针,此指针指向sq对象,而sq.VPTR又指向square::vtable。这个square::vtable的内容是
class square中所有虚函数(此例中只有square::Area())的调用指令的地址(根本不包括其它类的虚函数)。
B.如果class square具有多个虚函数时,如何能找到square:: Area( ) 的调用指令的地址呢?
事实上,编译系统早就在建立vtable时为虚函数Area( )在各个类的vtable中确定了它的位置偏移量
(所有类中都相同,可以是0或4或8)。编译系统根据此偏移量,就可找到square:: Area() 的调用指令的地址,
再按此地址就不难找到虚函数square:: Area( )本身。编译系统即按此进行联编。
编译系统在编译中就已完成此所谓动态联编的整个过程,所以它实质上应是静态的联编过程。
LESSON 17
1.有虚函数的对象的内存为“虚函数指针+数据成员”。数据成员=基类数据成员+本身数据成员。当基类指针指向该对象时,其可以访问的是“虚函数指针+基类数据成员”。而虚函数指针是该对象的虚函数指针,该对象的虚函数指针指向了
自己的虚函数表,所以即使基类指针指向该派生类对象时,还是只能调用该对象的函数。如果没有虚函数指针时,那么
基类指针调用该函数时,就会调用基类的函数,因为基类指针只能访问基类东西,也就是静态联编,在函数指令表中
找到基类的函数。如果有虚函数指针,那么编译器就会进行另一种动作,这些都是编译器编译时根据不同的情况进行
不同的动作。
2.eg:
class container {
protected:
double dimension;
public:
virtual double surface() { return 0.0; }
virtual double volume() { return 0.0; }
};
class cube : public container{
public:
virtual double surface()
{ return (6 * dimension * dimension); }
virtual double volume()
{ return (dimension * dimension * dimension); }
};
class sphere : public container{
public:
virtual double surface()
{ return (4 * PI * dimension * dimension); }
virtual double volume()
{ return ( PI * dimension * dimension * dimension * 4/3); }
};
void main()
{
container *ptr; //1
cube obj1(4); //2
ptr = &obj1; //3
cout<<"cube's surface is "<<ptr->surface( )<<endl; //4
cout<<"cube's volume is "<<ptr->volume( )<<endl; //5
sphere obj2(5); //6
ptr = &obj2; //7
cout<<"sphere's surface is "<<ptr->surface( )<<endl; //8
cout<<"sphere's volume is "<<ptr->volume( )<<endl; //9
}
A.主程序第一句建立基类指针ptr。
B.主程序第二句建立对象obj1并且初始化。
C.主程序第三句将基类指针ptr指向对象obj1。
D.主程序第四句调用函数ptr->surface( ),编译系统通过虚指针obj1.VPTR找到虚地址表obj1.vtable,
从而找到cube :: surface( )的调用指令地址,进而通过该调用指令来调用函数cube :: surface( )。
E.主程序第五句调用函数ptr->volume( ),编译系统通过虚指针obj1.VPTR找到虚地址表obj1.vtable,
通过虚地址表obj1.vtable表中的偏移地址找到cube :: volume( )的调用指令地址,进而通过该调用指令
来调用函数cube :: volume( )。
3.静态的成员函数不能为虚函数。因为它没有this指针,同时它由该类的所有对象共享,无法由单个对象专用。
4.当一个类中包含至少一个纯虚函数时,该类即是抽象类。而抽象类是不能建立对象的。
5.C++语言多态性的另一种表现是模板,也称为参数多态性。其实就是将类型进行参数化。
6.eg:
template <typename T>
class array {
array(int s);
~array();
};
template <typename T>
array<T>::array(int s){
}
7.模板分为函数模板和类模板。
8.函数模板实例化为模板函数。类模板实例化为模板类,模板类实例化为对象。
9.在使用函数模板时,首先函数模板中参数类型被预定义数据类型或用户自定义数据类型所替代,
也即函数模板被实例化(instantiated)为模板函数,然后该模板函数再被调用。
可以推测,编译器首先将通过类型来将其进行实例化,有多少种类型实例化多少种函数,然后具体调用。
模板达到了代码的复用,但是并没有使编译后的代码减少了,只是编译器帮我们做了这些事。
10.重载和模板都存在时的调用顺序:先重载,再模板,后相近。
在混合使用重载的函数和模板时,应当避免二义性。此时它们的调用顺序是:
A.先调用匹配的重载函数。
B.如无匹配的重载函数,则调用匹配的函数模板。如果找到,则将其实例化,建立一个模板函数。
C.如再匹配不上,则调用相近的重载函数,此时可能丢失精度。
11.重载函数与函数模板都属于多态性机制,它们的区别如下:
(1)主函数调用重载函数和模板函数的格式完全相同。
重载函数的优点是能够对不同数据类型使用不同程序逻辑进行类似操作。
例如用于求取int或double的最大值的程序逻辑和操作就和求取char或char*的最大值的程序逻辑和操作差别很大。
而函数模板无法处理。如果每种数据类型的程序逻辑和操作相同,则使用函数模板将会更加简洁和方便。
模板是一种代码产生机制。
(2)重载函数中,无论它们使用与否,全部都存于代码区中,都占用空间。而在函数模板中,
只当使用一个具体参数类型时才建立一个模板函数,而当使用另一个具体参数类型时再建立另一个模板函数。
编程方便,使用灵活,效率较高。
LESSON 18
1.类模板的派生eg1:基类的类模板派生出派生类的类模板时,其构造函数的初始化列表要求一定格式。
template<typename T>
class A {
public:
A(T a) { cout << "CONS_A:" << a << endl; }
};
template<typename S, typename T>
class B:public A<T> {
public:
B(S a, T b) : A<T>(b) { cout << "CONS_B:" << a << endl; }
};
template<typename X, typename Y, typename Z>
class D:public B< X, Y >{
public:
D(X a, Y b, Z c) : B<X, Y> (a, b) { cout << "CONS_D:" << c << endl; }
};
void main()
{
D< int, double, int > obj1(8, 4.4, 16);
D< int, char*, double > obj2(4, "good", 16.8);
}
eg2:基类和派生类分别使用不同数据类型
template <typename T>
class A{
public:
void f(T a) { cout << a << endl; }
};
template< typename X, typename Y>
class B:public A<Y>{
public:
void g(X b) { cout << b << endl; }
};
void main(){
B < char, int > bb;
bb.f(10);
bb.g('A');
}
2.typeid运算符,在c++中,typeid用于返回指针或引用所指对象的实际类型。
eg1:用typeid获取类型名
class Base{
public:
virtual void vvfunc() {}
};
class Derived : public Base {};
int main() {
Derived* pd = new Derived;
Base* pb = pd;
cout << typeid( pb ).name() << endl; // prints "class Base *"
cout << typeid( *pb ).name() << endl; // prints "class Derived"
cout << typeid( pd ).name() << endl; // prints "class Derived *"
cout << typeid( *pd ).name() << endl; // prints "class Derived"
}
eg2:用typeid判断模板参数类型。
template<class T>
void Test(T t){
if (typeid(T).name() == typeid(double).name()) {
cout << "correct" << endl;
}
}
int main() {
Test<double>(12.2);
return 0;
}
LESSON 19
1.使用系统的异常类class exception,为此,须包含头文件<exception>。eg:
void main(){
try {
cout << "The quotient of 18/9 is " << quotient ( 18, 9 ) << endl;
cout << "The quotient of 18/0 is " << quotient ( 18, 0 ) << endl;
cout << "The quotient of 4.5/9 is " << quotient ( 4.5, 9 ) << endl;
}
catch ( exception ex ){
cout << ex.what( ) << '\n';
}
}
2.用户自己定义系统class exception的派生异常类。
eg:
class Except_derive : public exception{
char *message;
public:
Except_derive(char * ptr) { message = ptr; }
virtual const char * what( ) const { return message; }
void handling ( ) {
cout << "Enter divisor(分母)again : ";
cin >> divisor;
}
};
void main() {
try {
if ( divisor == 0 ) throw Except_derive( "divide by zero!" );
cout<< "The quotient is: " <<(numerator/divisor)<<endl;
flag = false;
}catch ( Except_derive ex )
{ // exception handler
cout << "Exception : " << ex.what( ) << '\n';
ex.handling ( );
}
}
LESSON 20
1.eg:try(}{catch ( except & ex ) 和 try{} catch ( except* ex )
(1)使用异常类对象指针捕获异常时,一般在堆区中分配空间,因此当异常处理完毕后,必须删除堆区内的异常对象,
否则将造成资源泄漏。
(2)使用异常类对象引用捕获异常时,由于异常类对象是在栈区中建立的,因此当异常处理完毕后程序
退出捕获程序块时,异常对象将被自动删除。
LESSON 21
1.cout是一个对象,其中重载了运算符<<。
2.类ostream中派生出类ostream-withassign,而ostream-withassign建立了三个对象:cout、cerr和clog。
这就是标准库中定义的三个输出流。
其中cout是标准的输出流,而cerr和clog则与标准错误流相关。
其中,cerr是非缓冲输出流,发送给它们的任何数据都立即输出。而cout和clog是缓冲输出(buffered output)流。
3.整句的cout,clog和cerr输出语句的运行都可分为三个阶段:入栈阶段,缓存阶段和输出阶段。
(1)入栈阶段
此阶段由主函数main完成。主函数按照该语句的逆序将整条输出语句中需要输出的各项数据以及数据的存放地址压入
栈中。其中:
(a)如果数据是单个数据,直接将其压入栈中。其中整型数据或单个字符占四个字节,按照4字节边界原则运行。double
占8个字节等等。
(b)如果数据是数组,则系统将数组放置到数据区,并将此数组的首地址压入栈中。
(c)如果数据是一个函数运行的返回值,则系统执行该函数,并根据该函数返回值的数据类型,按照上述规则压入栈中。
eg:
int i = 10;
cout << i << ‘:’ << ++i << endl; // 输出11 : 11
该语句从后向前将数据压入栈中。入栈顺序为:函数endl的地址,11(i自增后的值),‘:’的ACSCLL码值,11(i)。
之所以要逆向入栈,是为了在下一个阶段(缓存阶段)能够首先从栈中获得排在输出语句前面的数据或数据地址。
(2)缓存阶段
此阶段由运算符"<<"完成。它顺序的从栈区将所存的数据或按照数据地址找到数据本身,并将该项完整数据存入位于
堆区内的缓存。如果缓存是空的,则该数据被放置于缓存的首地址。如果缓存内已有其它数据,则该数据被放置于
缓存内已有数据之后。
(3)输出阶段
此阶段由"<<"调用flush函数等来完成,它们最终调用硬件接口函数__imp__WriteFile,直接将数据输出至标准输出
设备。由于函数__imp__WriteFile无法访问堆区,因此必须再将数据从堆区转存到栈区,才能使用该函数进行调用。
flush函数顺序地从缓存(堆区)内取出所存的全部数据,重新压入栈区另外的地址,以备输出。接着函数__imp__WriteFile
直接将位于栈区新地址处的全部数据输出至标准输出设备。
4.缓冲输出流对象(cout和clog)和非缓冲输出流对象(cerr),它们在只能给输出语句时的差别在于:
(1)每当非缓冲输出流对象cerr语句执行时,在入栈阶段之后,它调用重载运算符函数"<<",缓存阶段
和输出阶段连起来执行,然后直接将全部数据输出至标准输出设备。
(2)每当缓冲输出流对象clog调用"<<"重载运算符函数时,一般情况下只执行入栈阶段和缓存阶段,
只将数据存入缓存而不输出数据,知道达到下列的四个条件之一时才执行输出阶段的操作。
5.在缓冲输出流中,只当满足以下四个条件中之一时,缓存才把所存数据向文件(即标准输出设备,
缺省为显示终端屏幕)输出:
(1)第一个条件:调用flush函数: flush函数是ostream的成员函数。此处它用于将输出流刷新,从而将流缓冲区的
内容输出到与流相关的输出设备中。
eg:
void main(){
int i=12345;
clog<<"ABCDE";
cout<<i<<endl;
clog<<flush;
}
/* Results:
12345
ABCDE*/
(2)第二个条件:程序结束
(3)第三个条件:缓存溢出(一般为512个字节)后自动将数据输出,并清除缓存,准备接收其它数据。
(4)第四个条件:调用endl,但不是"\n"。(endl函数调用了flush函数,除了刷新输出流之外,
还执行了回车操作。)
6.输出流只有缓冲的。运算符">>"在读取数据时,被输入的数据之间的缺省隔离符是空格或者换行符。
当输入非有效字符(例如当输入整形数据时,输入的小数点字符即为非有效字符)时,此非有效字符
也被认为是一个隔离符。
此时,上一个数值的输入操作结束。如果此非有效字符正好是下一个输入数值的有效字符则此非有效字符
被存入缓存内,作为下一次输入字符的有效字符。
7.cin用于读取字符串时,应该指出:所留出的缓存空间必须是在栈区或堆区内,而不应在数据区内。
eg:导致崩溃代码:
char* c_str = "adsf";
cin >> c_str;
8.自定义输入函数
当输入错误字符时,将可能造成输入错误,并使运算符函数cin返回零值。为解决此问题,用户可自己定义输入函数,
将错误地输入的字符筛选掉,而只接受有效字符。
[例1]用于浮点数的输入函数:只接收数字符和句点符
// 将输入的字符串转换为一个浮点型数据。
// 当输入小数点以外的其他非数字字符时,该程序能够越过它,不予理睬。
// 另外,如果你输入出错,可使用键盘右上角的'backspace'键,将错字符删去。
// 当输完一个完整数据之后,可用回车键(CR),函数conv( )就进行转换,
// double atof(char *)在读入12...34时,将会把它转换为12
#include <iostream.h>
#include <stdlib.h> //for double atof(const char *)
#include <conio.h> //for getch( )
double conv( )
{
char keych;
char arr[12];
for(int i=0;;)
{
keych = getch(); // read the low-byte keystroke
if ( keych == 13) //CR key
{
cerr<<endl;
break;
}
if ( ( (keych>='0') && (keych<='9') ) || (keych == 8) || (keych == 46) )
{
cerr<<keych;
if ( keych == 8) //'backspace' key
{ i--; cerr<<' '<<keych; //erase unwanted char and go back again
}
else arr[i++] = keych;
}
}
arr[i] = '\0';
return atof(arr);
}
void main()
{
double d, e, f;
cerr<<"Please input three double values:(CR)";
d = conv( );
e = conv( );
f = conv( );
cout << d << " " << e << " " << f <<endl;
}
/* Results:
Please input three double values (CR) : 12.56
12.34
12.78
12.56 12.34 12.78
*/
8.流输入运算符”>>” 重载
eg1:
#include <iostream.h>
class three
{
int x, y, z;
public:
three (int xi, int yi, int zi)
{
x = xi;
y = yi;
z = zi;
}
void print()
{
cout<<"x="<<x<<" y="<<y<<" z="<<z<<endl;
}
friend istream& operator >>(istream& input, three& obj);
};
istream& operator >>(istream& input, three& obj)
{
cout<<"Input coordinates x, y, z:";
input>>obj.x;
input>>obj.y;
input>>obj.z;
return input;
}
eg2:
double conv( )
{
char keych, prevch;
char arr[12];
int n=0; //by何海清
for(int i=0;;)
{
keych = getch(); // read the low-byte keystroke
if(n>=1) //by何海清
{
if(keych==46)
{continue;}
}
if ( keych == 13 || (keych == 32) ) //CR key or space key
{
cerr<<keych;
if ( keych == 13 ) cerr<<endl;
break;
}
if ( ( (keych>='0') && (keych<='9') ) || (keych == 46) //char '.'
|| (keych == 8) )
{
cerr<<keych;
if(keych==46) n++; //by何海清
if ( keych == 8) //'backspace' key
{ i--; cerr<<' '<<keych;
//erase unwanted char and come back again
if ( prevch == 46) n=0; //when erased char is '.'
}
else
{ arr[i++] = keych;
prevch = keych; // for erasure of '.'
}
}
}
arr[i] = '\0';
return atof(arr);
}
class istream_user
{
public:
istream_user& operator>>(double &d) { d = conv( );
return *this; }
}