一 类的初始化
多数类都提供了一个特殊的缺省构造函数default constructor 它不需要指定初始值典型情况下如果类对象是由缺省构造函数初始

化的则我们可以认为它还没有被初始化。

class A
{
public:
int a;

int b;
};
类A不需要构造函数,因为它的所有数据成员都是公有的。

int main()
{
A test={0,1}; //是正确的,根据数据成员被声明的顺序这些值按位置被解析。
getchar();
return 0;
}

显示初始化表两个主要缺点:
a 只能被应用到所有数据成员都是公有的类的对象上。
b 需要程序员显示的干涉

二 类的构造函数

1 构造函数不能指定返回类型,并且与类同名

2 一个类中可以声明多个构造函数

3 构造函数声明中的参数名不是必须的。Account( const char*, double=0.0 );

4 为构造函数指定实参有三种等价形式
Account acct1( "Anna Press" );
Account acct2 = Account( "Anna Press" );
Account acct3 = "Anna Press";//只能用于单参数

5 只有当没有构造函数或声明了缺省构造函数时我们才能不指定实参集来定义类对象

6 类对象的动态数组在分配内存的时候也要求或
者有缺省构造函数或者没有构造函数例如如果用户这样写则我们新定义的Account
类就会失败
// 错误: 要求缺省构造函数
Account *pact = new Account[ new_client_cnt ];

7 类的初始化列表
类的初始化有一种可替换的语法成员初始化表member initialization list 是由逗号
分开的成员名及其初值的列表例如缺省的Account 构造函数可以这样写
// 使用成员初始化表的缺省 Account 构造函数
inline Account::Account(): _name( 0 ),_balance( 0.0 ), _acct_nmbr( 0 )
{ }

成员初始化表只能在构造函数定义中被指定而不是在其声明中。该初始化表被放在参表和构造函数体之间由冒号开始。

8 构造函数不能用const 或volatile 关键字来声明。被应用到类对象上的适当的构造函数与该对象是const 非const 或volatile 

无关。

只有当构造函数执行完毕。类对象已经被初始化的时候,该类对象的常量性才被建立起来。一旦析构函数被调用,常量性就消失了

。因此一个const 类对象在从其构造函数完成到析构函数开始这段时间内才被认为是const的,对volatile 类对象也一样。

9 缺省情况下单参数构造函数或者有多个参数除了第一个参数外其他都有缺省实参被用作转换操作符。
// 在某个头文件中
extern void print( const Account &acct );
// ...
int main()
{
// 把 "oops" 转换成一个 Account 对象
// 用 Account::Account( "oops", 0.0 )
print( "oops" );
// ...
}

10 explicit 修饰符通知编译器不要提供隐式转换
class Account{
public:
explicit Account( const char*, double=0.0 );
// ...
};
explicit 只能被应用在构造函数上

11 缺省构造函数是指不需要用户指定实参就能够被调用的构造函数这并不意味着它不能
接受实参只意味着构造函数的每个参数都有一个缺省值与之关联例如下列每个函数都
表示一个缺省构造函数
// 每个都是缺省构造函数
Account::Account() { ... }
iStack::iStack( int size = 0 ) { ... }
Complex::Complex(double re=0.0,double im=0.0) { ... }

当我们写
int main()
{
Account acct;
// ...
}
编译器首先检查Account 类是否定义了缺省构造函数以下情况之一会发生
1 定义了缺省构造函数它被应用到acct 上
2 定义了缺省构造函数,但它不是公有的。acct 的定义被标记为编译时刻错误:main()
没有访问权限
3 没有定义缺省构造函数,但是定义了一个或者多个要求实参的构造函数。acct 的定义
被标记为编译时刻错误:实参太少
4 没有定义缺省构造函数,也没有定义其他构造函数。该定义是合法的,acct 没有被初
始化,没有调用任何构造函数。

12 如果Account 类把它的所有成员都声明成公有的但没有声明构造函数
如果声明一个静态对象如下
// 静态范围
// 每个对象相义的内存被初始化为0
Account global_scope_acct;
static Account file_scope_acct;
Account foo()
{
static Account local_static_acct;
// ...
}
则保证成员被初始化为0 对非类对象也一样。

自我总结:
a 如果没有定义任何构造函数则不户对成员进行初始化
b static对象的成员被初始化为0
c 如果对象的部分被初始化,没被初始化的成员是未知随机分配的。

13 构造函数的可访问性由其声明所在的访问区来决定。可以通过将构造函数放到非公有访问区,从而限制或禁止某种形式的对象创

建。
非公有构造函数的主要用处是:
a 防止用一个类的对象向该类的另一个对象做拷贝。
b 指出只有当一个类在继承层次中被用作基类,而不能直接被应用程序操纵时,构造函数才能被调用。

14 拷贝构造函数
用一个类对象初始化该类的另一个对象被称为缺省按成员初始化。
在概念上,一个类对象向该类的另一个对象作拷贝是通过一次拷贝每个非静态数据成员实现的。

可以通过提供特殊的拷贝构造函数来改变缺省的行为。

成员的初始化自己总结:
a 对于静态成员,只能在类外进行显示定义(const static有些例外)
b 对于const、引用成员只能在成员初始化列表中初始化


三 类的析构函数

1 析构函数是一个特殊的由用户定义的成员函数,当该类的对象离开了它的域,或者delete表达式应用到一个该类的对象的指针上

时,析构函数会自动被调用。
析构函数的名字是在类名钱加上波浪号(~),它不返回任何值也没有任何参数,所以它不可以被重载。

2 如果一个类的数据成员是按值存储的,则无需析构函数。

3 动态分配的对象
程序员用new表达式创建动态分配的对象,用delete表达式结束此类对象的生命期。

动态分配的对象可以使单个对象,也可以使对象的数组。
 
4 单个对象的动态分配与释放
int *pi=new int;

特点一:分配的对象没有名字
特点二:分配的内存是未初始化的。

delete pi;
释放内存

// 这样做有必要吗
if ( pi != 0 )
delete pi;
没有必要,因为编译器会自动检测

在delete表达式之后,pi被称为空悬指针,即指向无效内存的指针。一个比较好的办法就是在指针指向的对象被释放后,将该指针

设置为0。

delete表达式只能被用在指向的内存是用new表达式从空闲存储区分配的指针上。

与动态内存分配的常见错误
a 应用delete表达式失败,内存泄漏
b 对同一内存区应用了两次delete表达式。这通常发生在两个指针指向同一个动态分配对象的时候。
c 在对象被释放后读写该对象。

5 auto_ptr
auto_ptr是C++标准库提供的类模版,可以帮助程序员自动管理用new表达式动态分配的单个对象。

#include<memory>
auto_ptr< type_pointed_to > identifier( ptr_allocated_by_new );
auto_ptr< type_pointed_to > identifier( auto_ptr_of_same_type );
auto_ptr< type_pointed_to > identifier;

auto_ptr< int > pi( new int( 1024 ) );
auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) );

auto_ptr< string > pstr_auto2( pstr_auto );//利用pstr_auto初始化pstr_auto2,那谁负责删除分配的内存呢?
当定义pstr_auto 时它知道自己对初始化字符串拥有所有权并且有责任删除该字符串。
当一个auto_ptr 对象被用另一个auto_ptr 对象初始化或赋值时,左边被赋值或初始化的对象就拥有了空闲存储区内底层对象的所

有权,而右边的auto_ptr 对象则撤消所有责任。于是,在我们的例子中,将是用pstr_auto2 删除字符串对象,向不是pstr_auto 

pstr_auto 不再被用来指向字符串对象。

类似的行为也发生在赋值操作符上已知下列两个auto_ptr 对象
auto_ptr< int > p1( new int( 1024 ) );
auto_ptr< int > p2( new int( 2048 ) );
赋值操作符可以将一个auto_ptr 对象拷贝到另一个中如下所示
p1 = p2;
在赋值之前由p1 指向的对象被删除赋值之后p1 拥有int 型对象的所有权该对象值为2,048 p2 不再被用来指向该对象


get()返回auto_ptr对象内部的底层指针。
if(p_auto_int.get()!=0)

reset()函数
p_auto_int.reset(new int(1024));

我们不能够在auto_ptr 对象被定义之后再用new 表达式创建对象的地址来直接向其赋值:
void example()
{
// 缺省, 用0 初始化
auto_ptr< int > pi;
{
// 不支持
pi = new int( 5 );
}
}

/////为了重置一个auto_ptr对象,我们必须使用reset()函数。我们可以向reset()传递一个指针,如果不希望设置该auto_ptr对象

的话,可以传进一个0值。

auto_ptr< string >
pstr_auto( new string( "Brontosaurus" ) );
// 在重置之前删除对象 Brontosaurus
pstr_auto.reset( new string( "Long -neck" ) );
在这种情况下用字符串操作assign()对原有的字符串对象重新赋值比删除原有的字符
率对象并重新分配第二个字符串对象更为有效
// 这种情况下重置的更有效形式
// 用 string 的 assign() 设置新值
pstr_auto->assign( "Long-neck" );

我们应该注意的:
a 不能用一个指向“内存不是通过应用new表达式分配的”来初始化赋值auto_ptr。如果这样做了,delete表达式会被应用在不是动

态分配的内存上。
b 不能让两个auto_ptr对象拥有空闲存储区内同意对象的所有权。

release()操作允许将一个auto_ptr 对象的底层对象初始化或赋位给第二个对象而不会
使两个auto_ptr 对象同时拥有同一对象的所有权release()不仅像get()操作一样返回底层对
象的地址而且还释放这对象的所有权前面代码段可被正确改写如下
// ok: 两个对象仍然指向同一个对象
// 但是, pstr_auto 不再拥有拥有权
auto_ptr< string >
pstr_auto2( pstr_auto.release() );

6 数组的动态分配与释放
new表达式返回指向数组第一个元素的指针
int *pia=new int[1024];//分配一个含有1024个元素的数组,未被初始化
一般的,在空闲存储区上分配的数组不能给出初始化值集。

在空闲存储区中创建的内置类型的数组必
须在for 循环中被初始化即数组的元素被一个接一个地初始化:
for ( int index = 0; index < 1024; ++index )
pia[ index ] = 0;

char *str1 = new char[ strlen( errorTxt ) + 1 ];在运行时刻才被计算的表达式代替维数

new一个二维数组的两种形式:

1. A (*ga)[n] = new A[m][n]; 
... 
delete []ga; 
缺点:n必须是已知 
优点:调用直观,连续储存,程序简洁(经过测试,析构函数能正确调用) 

2. A** ga = new A*[m]; 
for(int i = 0; i < m; i++) 
ga[i] = new A[n]; 
... 
for(int i = 0; i < m; i++) 
delete []ga[i]; 
delete []ga; 
缺点:非连续储存,程序烦琐,ga为A**类型 
优点:调用直观,n可以不是已知 

7 常量对象的动态分配与释放
const int *pci=new const int(1024);
const对象必须被初始化
用new表达式返回的值作为初始值的指针必须是一个纸箱const类型的指针。

不能new一个const数组,一个简单的原因是没法对new的数组进行初始化(除了类数组)。

8 显示的析构调用
char *arena = new char [ sizeof Image];
Image *ptr = new (arena) Image("Quasimodo")。相反,ptr 被赋值为与arena 相关联的地址,通过ptr,内存被解释为一个Image 

类对象。

记住,只有当delete 表达式中的指针指向一个带有析构函数的类类型时,编译器才会调用析构函数。

四 类对象数组和vector

1 对于在堆中分配的类对象数组的元素我们没有办法提供一组显式的值来做初始化。
  如果希望支持通过new 表达式分配数组则类必须提供一个缺省构造函数或不提供构造函数

Account *pact = new Account[ 10 ];创建了一个在堆中分配的包含10 个Account 类对象的数组它们都用Account 类缺省构
造函数初始化。

delete [] pact

2 堆数组的初始化
缺省情况下在堆中分配的类对象数组的初始化要求两步:
a 实际分配数组,如果定义了缺省构造函数,则把它应用到每个元素上
b 每个元素后来被赋予一个特定的值。

3 类对象的vector
vector<Point>vec(5);
元素的初始化过程如下:
1 创建一个底层类类型的临时对象在其上应用该类的缺省构造函数
2 在vector 的每个元素上依次应用拷贝构造函数用临时类对象的拷贝初始化每一个类对象
3 删除临时类对象

五 成员初始化表
1 使用初始化列表和在构造函数中使用数据成员的赋值之间有什么区别?
a 两种实现的结果是一样的。区别是成员初始化表只提供该类数据成员的初始化,在构造函数体内对数据成员设置值是一个赋值操

作。区别的重要性取决于数据成员的类别。

构造函数分为两个阶段:
隐式或显示初始化阶段,以及一般的计算阶段。计算阶段由构造函数体内的所有语句组成。在计算阶段,数据成员的设置被认为是

赋值而不是初始化。

初始化阶段可以使显示的或隐式的,取决于是否存在成员初始化列表。隐式初始化阶段按照声明的顺序依次调用所有基类的缺省构

造函数。然后是所有成员类对象的缺省构造函数。

对于类对象,在初始化和赋值之间的区别是巨大的。成员类对象应该总是在成员初始化列表中被初始化。

const类型和引用类型必须在成员初始化列表中初始化,其他类型的参数无所谓。

因为当构造函数体开始执行时,所有的const和引用的初始化必须都已经发生。

每个成员在成员初始化表中只能出现一次初始化的顺序不是由名字在初始化表中的顺序决定而是由成员在类中被声明的顺序决定
按成员赋值

// 拷贝赋值操作符的一般形式
className&
className::
operator=( const className &rhs )
{
// 保证不会自我拷贝
if ( this != &rhs )
{
// 类拷贝语义在这里
}
// 返回被赋值的对象
return *this;
}

这里条件测试是
if ( this != &rhs )
应该防止一个类对象向自己赋值。因为对于先释放与该对象当前相关的资源,以便分配与被拷贝对象相关的资源这样的拷贝赋值操

作符,拷贝自身尤其不合适。

2 当一个类对象被赋值给该类的另一个对象时如
newAcct = oldAcct;
下面几个步骤会发生:
a 检查该类,判断它是否提供了一个显示的拷贝赋值操作符
b 如果是,则检查访问权限,判断是否在这个程序部分它可以被调用
c 如果它不能被调用,则产生一个编译时刻错误,否则,调用它执行赋值操作。
d 如果该类没有提供显示的拷贝构造操作符,则执行缺省按成员赋值。
e 在缺省按成员赋值下,每个内置或复合类型的数据成员被赋值给相应的成员。
f 对每个类成员对象,递归a到e
 posted on 2009-06-25 22:08  清水湾  阅读(574)  评论(0编辑  收藏  举报