C++——设计与演化——读书笔记
<<c++设计与演化>>
1.c++的保护模式来自于访问权限许可和转让的概念;
初始化和赋值的区分来自于转让能力的思考;
c++的const概念是从读写保护机制中演化出来.
2.BCPL注释:
CPL(Combined Programming language,组合编程语言):CPL是一种在ALGOL60基础上更接近硬件的一种语言。CPL规模大,实现困难。
BCPL(Basic Combined Programming language,基本的组合编程语言):BCPL是对CPL进行简化后的一种语言。
3.一般地说在世界上,事情并不总是处在很好的状态,要改进它们,有许多事情是可以做的。
c++是为了解决一个问题,而不是想证明一种观点,而它的成长又能够服务于它的使用者.
4.一个类就是一个类型.
5.对象的分配可以有三种方式:
在栈上(作为自动对象),在固定地址(静态对象),或者在自由存储区(在堆,或者说动态存储区)。
与c语言不同的是,带类的c自由存储的分配和释放提供了特定的运算符new和delete.
6.C的意图是采用按名字物价而不是按结构等价.
struct A { int x; int y; };
struct B { int x; int y; };
struct A* pa;
struct B* pb;
pa = pb; /* error : A* expected */
pa = (struct A*) pb; /* OK:explicit conversion */
所以,按名字等价是c++类型系统的基石, 而布局相容性规则保证了可以使用显示转换,以便能提供低级的转换服务.
这样导出了"惟一定义规则":在c++语言中,每个函数,变量,类型,常量等都应该恰好有一个定义.
6.设法保证忽略C++的警告将被看成是一种愚蠢行为.
7.语言设计并不是第一个原理出发的设计,而是一种需要经验,试验和有效工程折衷的艺术.
8.C++对于C多名字空间的解决办法是:
一个名字可以指称一个类,同时也可以指称一个函数,或者一个变量。如果某个名字真的同时指称两种不同的东西,那么这个名字本身的指称的就是那个非类的东西, 除非在前面明显地加上关键字struct, union或者 class.
9.虚函数在时间和空间方面都与常规函数同样有效,即使没有虚函数概念,在带类的C里的派生类也很有用,可以用于从老的构造出新的数据结构,用于将操作与结果关联起来.
10.对象布局模型
把一个类里定义的一集虚函数定义为一个指向函数的指针数组,这样,对虚函数的简单地也就是通过该数组一个间接调用。对每个有虚函数的类都存在一个这样的数组,一般称为虚函数表或者vtbl,这此类的每个对象都包含一个隐式指针,一般称为vprt,指向该对象的类的虚函数表.
class A {
public:
virtual void f();
virtual void g (int);
virtual void h (double);
private:
int a;
};
class B : public A{
public:
int b;
void g(int);
virtual void m(B*);
};
class C : public B{
public:
int c;
void h(double);
virtual void n(C*);
}
类C的一个对象看起来:
+--------------------------+
| a | +----------------------+
| vptr ------>| --> | &A::f |
| b | | &B::g |
| c | | &C::h |
+--------------------------+ | &B::m |
| &C::n |
+----------------------+
对虚函数的调用被编译系统翻译成一个引用.
void f(C* p)
{
p ->g(2);
}
变成:
(*(p ->vptr[1])) (p, 2);
11.用省略号制止类型检查是其中最激烈的也是最不被推荐的东西.
12.变量的作用域:
for (int i = 0; i < 10; ++i)
{
}
i = 0;
在c++语言规范里,i的作用域不应该超过for里{}之外,但在vs2003里可以.
if ,while也一样.
13.面向对象的程序设计是利用继承机制的程序设计;数据抽象是使用用户定义类型的程序设计;面向对象的程序设计将能够而且应支持数据抽象.
14.类型重新定义规则:
typedef char* T;
class Y {
T f() { T a = 0; return a; }
typedef int T;
};
c++语言规则: 如果一个类型已在某个类中使用过,就不能在那里重新定义.
<上面T,应该是重定义,但在vs2003里错误提示: 无法从Y::T转为T,因为T f()返回的是最上面的T,而函数体内的是类里定义的T,所以,此处错误有分歧,到底是不能返回不同的类型还是后面那个T是重定义?>
<此处应按c++标准,不能进行类型的重定义>
15.重写规则:对于以在线(online)方式定义的成员函数进行分析时,就像它们是在类声明之后定义那样去做.
如:
class TT
{
A f();
void g() { A a; }
typedef int A;
};
A f(); 是错误的,因为A还没有声明,而void g(){ A a; }是正确的,因为符合上面写的规则.
16.基于编译器出错:
class T{}; 如果用T做为类名,在vs2003里出现内部编译器出错信息.
17.c++定义规则(1993):
(1).在一个类里一个被声明的名字的作用域不仅包括这个名字声明之后的正文,还包括所有的函数体,默认参数,以及在这个类里的构造函数初始化(包含嵌套的类和这些部份),但不包括这个名字本身的声明式.
(2).在类S里使用过的一个名字,在它的上下文中或在S的完整作用域中重新求值的时候引用的都必须是同一个声明,S的完整作用域由类S本身,S的基类以及所有包含着S的类组成。这通常称为"重新考虑规则".
(3).如果在一个类里重新排列了成员的声明,产生的是另一个符合1和2的合法程序,那么该程序是无定义,这通常被称为"重新排序规则".
18.临时变量
void f (string s1, string s2)
{
printf ("%s", (s1+ s2).c_str());
const char* p = (s1 + s2).c_str();
printf ("%s", p);
}
const char* p = (s1 + s2).c_str();此句具体的操作:
(1).生成一个string对象
(2).调用拷贝构造
(3).调用重载+=
(4).析构函数
所以p 指针在此语名结束后,根本不能获取临时字符串对象的地址<临时对象的生命期不会到此函数体的结束处>,因为临时对象的生命期结束.
此外,string库里的+操作是返回一个对象的值,不是引用.
19.放置:
前提: (1).需要一种机制把对象安放到特定的地址上(把一个表示进程的对象放到特定的硬件地址上);
(2).需要一种机制在某种特定区域里分配对象(在一个多处理器系统的共享存储中,或由某个特定管理器控制的区域中分配对象)。
解决方案:
重载new运算符,又c++规则里不能对delete进行多样的重载.
new的重载如下:
class X
{
public:
void* operator new (size, void* p) { return p; }
};
void* buf = (void*)0xF00F; //取特定的地址
X* p1 = new(buf)X; // 这里就是在特定的地址上分配对象
X* p2 = new(buf)X; // 此时p1和p2对象的地址都为0xF00F;
new 的第一个参数是对象的长度,由编译器递给new,buf是放置对象的地址,p对内存分配函数或对象的引用.
// 以上格式为放置语法格式
放置机制使new不再只是一种简单的存储分配机制,因为可以给特定的存储位置关联上任意的逻辑性质,使得new起一种通用资源管理的作用.
分配区的一种重要应用是提供特定的存储管理定义.
释放问题:
不能期望用户有关对象是如何分配的,用户就根本不要去释放此对象,这是特殊分配场地的 一种用途.
但是可以用p1 ->X::~X();语句释放<这是一个特殊的调用格式>
所以上面类x也不要重载new运算符(那只是观察对象是怎样分配内存区).
20.C++区分了5种“匹配”:
(1).匹配中不转换或者只使用不可避免的转换(例如,从数组到指针,函数名到函数指针,以及T到const T);
(2).使用了整数提升的匹配(像ANSI标准所定义的。也就是说从char到int,short到int以及对应的unsiged类型)以及float到double;
(3).使用了标准转换的匹配(例如从int到double,derived*到base*,unsigned int 到int);
(4).使用了用户定义转换的匹配(包括通过建构造函数和转换操作);
(5).使用了在函数声明里的省略号...的匹配.
以上可以在ARM里有更好的匹配规则
21.ARM对指针描述:
空指针并不一定用与整数0同样的二进制式表示.
C++具有足够强的类型语言,一个像空指针这样的概念完全可以采用实现者选定的任何方式表示.
所以p = 0,给指针p赋了空指针值,但并不表示必然和整数0完全一样.
22.怎样实现只能在堆上分配一个对象或怎样只能在全局或栈上分配一个对象
(1).只能在堆上分配
class X {
public:
X() {}
static void free(X* p) { delete p; }
private:
~X(){}
};
X x; //错误
X* p = new X(); //OK
X::free(p); //析构
它也能阻止派生一个新类;
(2).只能在全局或栈上分配一个对象
class Y{
public:
Y() {}
private:
class D {};
void* operator new(size_t, D){}
};
或
class Y
{
public:
Y() {}
private:
void* operator new(size_t){}
};
X x; //OK
X* p = new X(); //错误
23.怎样阻止一个类派生一个子类
class usable;
class unable_lock{
friend usable; // 变成友元可以访问私有构造函数
private:
unable_lock() {}
};
class usable : public virtual { /*这里必须要virtual关键字*/ unable_lock
public:
usable() { }
};
class DD : public usable{};
usable ua; //OK
DD dd; //错误,不能实例化
虚拟继承后改变了构造的顺序:unable_lock -> usable ->DD;
采用非虚拟继承时顺序:usable ->调用usable的父类unable_lock的构造函数;
所以虚拟继承时,DD类不能调用unable_lock里的构造函数,构造函数为私有.
假如:
class X1{
public:
X1() {}
};
class X2_1 : public virtual X1{
public:
X2_1() {}
};
class X2_2 : public virtual X1{
public:
X2_2(){}
};
class X3 : public X2_1, public X2_2{
public:
X3(){}
};
X3 xs;
虚拟继承的顺序:X1 -> X2_1 -> X2_2 ->X3;
非虚拟继承的顺序:X2_1 ->X1 -> X2_2 ->X1 -> X;
采用虚拟继承时有两个作用:(1).改变顺序;(2).只有X1的一份拷贝,或者说只构造一次.
24.多重继承:
class AW : public W{};
class BW : public W{};
class CW : public AW, public BW{};
这叫独立的多重继承,
结构:
/----------AW --------- W
CW
\----------BW ----------W
虚继承:
class AW: public virtual W{};
class BW : public virtual W{};
class CW : public AW, public BW{};
结构:
/-----------AW----------\
CW W
\-----------BW----------/
类W的"虚"是AW和BW描述的那些派生的一种性质,而不是W自身的一种性质,每个virtual基类总表示一个对象.
25.一个正常的基类在一个给定派生类的每个对象的位置都是固定的.
一个虚基类的对象并不位于某个固定位置,必须通过一个间接才能访问.基类被定义一个无名成员.如果允许有虚数据成员,那么虚基类就该是虚数据成员的一个例子.
26.派生类的函数常常是基于同一函数在基类中的版本综合出来的,这一般称作方法组合.
27.抽象类概念的重要性在于它能使人对于用户和实现者做更清晰的划分,降低用户和实现者之间的偶合度。
28.const成员函数:
const成员函数可能用于const对象或非const对象,非const成员函数只能用于非const对象,
假如有对象X,此时就是让X的非const成员函数里的this指针指向X,而让其const成员函数里的this指针指向const X.
29.由于static成员函数并不关联于任何特定对象,因而不需要用特定成员函数的语法进行调用,在某些情况下,类被简单地娄做一种作用域使用,把不放进就是全局的名字放入其中,作用为它的static成员,以便使这些名字不会污染全局的名字,这是名字空间概念的一个起源.
30.指向数据成员的指针是一种表达C++的类布局方式的与实现无关的方式.
指向成员的针指实际上比一个偏移量(标识对象中一个成员的值)还要多一些东西.
31.RTTI(run-time type infomation).
32.运行时类型信息包括3个部份:
(1).一个运算符dynamic_cast,给它一个指向某对象的基类指针,它能够得到一个到这个对象的派生类指针,只有在被指对象确实属于所指明的派生类时,运算符dynmaic_cast才给出这个指针,否则返回0;
(2).一个运算符typeid,它对一个给定的基类指针识别被指对象的确切类型;
(3).一个结构type_info,作为与有关类型的更多运行时类型住处的挂接点(hook).
33.模板:
template <class T> class vector{
public:
vector(int);
T& operator[] (int);
T& elem(int i) { return v[i]; }
};
解释:
template 申请模板关键字,同时为模板类和模板函数提供了一种共有语法形式;
<>是为了强调模板参数具有不同的性质(将在编译时求值)
class T 用于指明类型参数的类型部份
模板是为生成类型提供的一种机制,本身并不是类型,也没有运行时的表示形式,所以对于对象的布局没有任何影响.
除了类型参数外,C++也允许非类型的模板参数,这种机制基本上被看着是容器类提供大小和界限所需的信息。
tempate<class T, int i> class buffer{
public:
buffer():sz(i){}
private:
T v[i];
int sz;
};
在那些运行时效率和紧凑性非常要紧的地方,传递大小信息允许实现者不使用自由空间.如果不能使用非类型的参数,用户就必须把关于大小的信息编码用到数组类型里.
template<class T, class A> class buffer{
public:
buffer():sz(sizeof(A) / sizeof(T)){}
private:
A v;
int sz;
};
buffer<int, int[700]> b;
在模板设计中,不允许名字空间和模板作为模板参数.
类型检测是在模板实例化的时刻进行.
模板实现是一种基于用户所描述的需要去生成类型的机制.
34.类模板可以有默认参数,模板函数则不可以
template<class T = int> class xyz{}; //OK
template<class T = int> T add(T& t){ return ++t; } //error
因为函数模板的模板参数是在调用的时候编译器根据实参的类型来确定的
35.就C++语言规则而论,由同一个类模板生成的两个类之间没有任何关系.
class x{};
class xx : public x{};
template<class T> class a{};
a<x> x1;
a<xx>x2;
x1 = x2; //error
x2 = x1; //error
36.成员模板函数不能是虚的
class shape
{
public:
template<class T> virtual bool get() const { return true; } //error be can't virtual
};
因为如果能用virtual合法化时,类的虚函数表会每次因使用者传递的类型不同,而添加新项.这意味着连接程度才能去构造虚函数表,并在表中设置有关的函数.