引子
全球有几百万人在使用c++,可是能称得上专家的没有几个。大多数人还是仅仅是为了解决自己领域内的问题而了解一下c++,仅仅使用了C++语言中一部分子集,有许多问题并不清楚。可是既然吃定了IT业这碗饭了,特别是经常用C++作为工作语言的工程师,很好的掌握C++是一个基本功。C++的语言要点很多,我在学习C++的过程中总结了一些需要引起重视的地方以及收集了几个难点,希望对学习C++的同志有所帮助。 这篇文章会动态地变化。
-
声明和定义的位置
-
C++的名字解释
-
-
内存对象模型
-
安全编程
本文将分阶段完成。
1)什么时候调用拷贝构造函数与拷贝赋值函数?
当要在创建一个类对象并用一个存在的对象初始化该对象时(包括直接定义的类对象以及在函数中的临时类对象)拷贝构造函数将被调用。当用一个对象向另一个对象赋值是,如果存在拷贝赋值函数的定义时,就会调用拷贝赋值函数。这些规则很简单, 可是大家看下面的例子,猜猜那个函数会被调用。
public:
ABC& ABC(const ABC& abc)...{...};
ABC& operator=(const ABC& abc)...{...};
}
ABC one;
ABC two = one; // 什么函数会被调用?
ABC three;
three = two; // 什么函数会被调用?
答案是: ABC two=one; 在这种情况下是由于要创建对象two会调用拷贝构造函数 ABC(const ABC&)。three=two会调用赋值函数 operator=(const ABC&)。
2. C++的对象布局
冯.诺依曼计算机是按指令一条条执行的,如何实现出C++面向对象及范型编程的特性,这是编译器要做的工作。在C++编译器内部都有个C++对象模型,各家的编译器可能有所不同,但是大同小异。这个对象模型,其实就是C++的对象布局模型,它规定了C++类变量及成员如何布置。C++编译器便在其定义的对象模型的基础上,对C++语言按照此模型进行展开(展开成C或者有其他中间语言,最后成为机器语言),在编译时或运行时对C++的对象操作进行支持。
查看下面的例子:
#include < iostream >
#include < string >
#include < typeinfo >
using namespace std;
// 根类
class Base ... {
int a;
public:
string discription;
//缺省构造函数
Base(void)...{
discription="Class Base";
printDisp();
a=1;
};
Base(Base&h)...{a=1;};//赋值函数
virtual ~Base(void)...{};
virtual void printDisp(void)...{cout<<discription<<endl;};
virtual void f(int)...{printf("Base::f");}; //虚函数
} ;
class DerivedOne: public Base ... {
public:
int done;
DerivedOne(void):Base()
...{
discription="Class DerivedOne";
printDisp();
done=3;
};
virtual ~DerivedOne()...{};
void f(void)...{printf("DerivedOne::f");}; //虚函数实现覆盖
virtual void g(void)...{f();}; //定义一个新的虚函数
} ;
class DerivedTwo: public Base ... {
int c;
public:
DerivedTwo(void)...{c=3;};
virtual ~DerivedTwo()...{};
virtual void h(int)...{};
} ;
// 多重继承
class DerivedThree: public DerivedOne, public DerivedTwo ... {
int d;
public:
DerivedThree(void)...{d=4;};
~DerivedThree()...{};
void f(int)...{printf("DerivedThree::f");};
} ;
int main( int argc, char * argv[])
... {
int ss;
Base base;
DerivedOne one;
DerivedThree three;
DerivedOne* pone=&one;
pone->g();
cin>>ss;
}
eclipse 编译完成后可以很容易看见对象的内存布局。
base的内存布局是
{_vptr$Base = 0x444d8c, a = 1, discription = {static npos = 4294967295, _M_dataplus = {<allocator<char>> = {<new_allocator<char>> = {<No data fields>}, <No data fields>}, _M_p = 0x7127dc "Class Base"}}}
virtual table pointer | _vptr$Base |
Base Data | a=1; discription={...} |
{<Base> = {_vptr$Base = 0x444d0c, a = 1, discription = {static npos = 4294967295, _M_dataplus = {<allocator<char>> = {<new_allocator<char>> = {<No data fields>}, <No data fields>}, _M_p = 0x722824 "Class DerivedOne"}}}, done = 3}
从中可以看出,如果一个类有虚函数那该对象会有一个虚函数表指针在数据布局中。
one的内存布局是:
Base Class data | _vptr$Base = ...... ; a=1; discription = ""; |
DrivedOne data | done = 3; |
一个对象构造时会先构造好基类的对象数据布局然后再构造自身的数据。
three的对象内存布局是:
{<DerivedOne> = {<Base> = {_vptr$Base = 0x22c9d8, a = 7405568, discription = {static npos = 4294967295, _M_dataplus = {<allocator<char>> = {<new_allocator<char>> = {<No data fields>}, <No data fields>}, _M_p = 0x22ce68 "/340/377/""}}}, done = 88}, <DerivedTwo> = {<Base> = {_vptr$Base = 0x270f0050, a = 0, discription = {static npos = 4294967295, _M_dataplus = {<allocator<char>> = {<new_allocator<char>> = {<No data fields>}, <No data fields>}, _M_p = 0x7 <Address 0x7 out of bounds>}}}, c = 5177344}, d = 5242904}
DrivedOne | Base object; done = ...; |
DrivedTwo | Base object; c = ...; |
大家可以看见,对象three有两个Base Object实例,为了避免这种情况,我们需要利用虚继承。将DerivedOne 和DerivedTwo改写代码如下:
class DerivedTwo: public virtual Base ... {...} ;
可以看到three的对象布局为:
{<DerivedOne> = {<Base> = {_vptr$Base = 0x445eb4, a = 1, discription = {static npos = 4294967295, _M_dataplus = {<allocator<char>> = {<new_allocator<char>> = {<No data fields>}, <No data fields>}, _M_p = 0x72284c "Class DerivedOne"}}}, _vptr$DerivedOne = 0x445e78, done = 3}, <DerivedTwo> = {_vptr$DerivedTwo = 0x445e94, c = 3}, d = 4}
你可以很明显地看到,只有一个Base object.
3. C++特性引入的背景理解
在学习C++中,不了解各种特性引入的原因和背景,就会陷入只见树木不见森林的境地。这里就一些常见的理解罗列一下。
3.1 C++类中引入了各种特殊的操作函数:构造/析构/赋值函数/复制构造函数/运算符重载
C++的类设计,希望尽量使得能够象内部类型一样地使用,可以满足:声明/定义一个变量;变量的初始化;象内部类型一样可以进行+,-,×,/,++,--等操作;可以赋值;可以在函数运算时完成传值,返回值,类型的隐式显式转换。 只有定义了如题目所列的这些类的操作函数,一个类对象才可以象内部类型一样自由使用。
4. 类的成员访问控制
对类成员的访问控制,好多人都看了定义觉得搞清楚了,其实比较迷糊。看下面的两个例子(from TC++PL)如果你答对了那就证明你理解了。
例子一:
protected:
char a[129];
//...
} ;
class Linked_buffer:public Buffer ... {/**//*...*/} ;
class Cyclic_buffer: public Buffer ... {
//...
void f(Linked_buffer* p) ...{
a[0]=0; //对吗?
p->a[0]=0; //对吗?
}
a[0]=0; 是对的,因为Cyclic_buffer父类的保护数据a是可以被继承类Cyclic_buffer访问的。
p->a[0]=0;就不对了,因为p是类Linked_buffer的指针,Cyclic_buffer不能访问Linked_buffer类父类的保护成员。
例子二:
private:
int a;
public:
print(A& obj)...{ cout<<a<<endl;
};
main()...{
A a1, a2;
a1.print(a2);
a2.print(a1);
}
有的人会觉得纳闷,a1.print(a2)函数会导致a1的print函数访问a2的私有成员。其实这是没有问题的,因为类的访问控制是基于类的,并不是基于对象的。因为a1,a2都属于同一个类,所以上面的程序没有问题。
类的访问控制总结如下:
private: 只能由该类中的函数、其友元访问,该类的对象也不能访问.
protected: 可以被该类中的函数、子类的函数、以及其友元访问,但不能被该类的对象访问
public: 可以被该类中的函数、子类的函数、其友元访问,也可以由该类的对象访问