c++类注意事项

1。const和引用成员数据必须在构造函数的初始化列表(initialize list)中初始化,不可以放在构造函数体中,也不可以在声明处初始化!(只有static const可以)例如:

 

/*
*@file: a.hxx
*/
#include

class A
{
     const int i;
    int& rx;

    public:
        A( const int);
};

/*
* file: a.cxx
*/
#include "a.hxx"

A::A( int x ) : i(100), rx(x) //ok
{
i=100;//error
rx = x;//error
}

int main()
{
    int var = 0;
    A a( var );

    return 0;
}

2.在const成员函数中,可以访问成员数据,但是不可以修改,否则导致会编译错误,因为每个成员数据都被加上了const属性!如果想在const类成员函数中修改成员变量的话,则可以用c++关键字mutable来修饰成员变量。如:
#include
class T
{
    mutable int i;
public:
    T():i(10){}
    void set( int ii ) const ;
    int get() const ;
};

void T::set( int ii ) const
{
    i = ii;      
}

int T::get() const
{      
    return i;
}

int main()
{
    T t;
    std::cout << t.get() << std::endl;
    t.set( 30 );
    std::cout << t.get() << std::endl;
    return 0;
}

3.static 成员数据的初始化要放在实现文件.cpp中,不能放在头文件.hxx,否则会违反C++一次定义原则,ISO C++标准禁止这种方式的初始化,但是static const可以!注意:const static 与 static const是一回事,如:

//header file: t.hxx
class T
{
static int x=0;//error iso c++ forbid.
const static int x = 0;//ok.
};

//other.cxx
#include “t.hxx”

int T::x = 0;//ok

4.指向static成员的指针的类型中不需要使用class scope修饰:

double* pd; //而不是:double T::*pd;
pd = &T::d;
---------
void (*pf)(); //而不是:void (T::*pf)();
pf = T::f;

5.如果在类T定义之前,使用T,则编译器会报错,可使用类的前向声明,不过使用类的前向声明是有限制的!如:
class T; //class forward declaration.

class S
{
    T t;//error
    public:
    S( T t ){}//error
    S( T& t ){}//ok
    S( T* t ){}//ok
    void printT( T* t, T& rt )//ok
    {
    cout << t->value() << endl;//error
    cout << rt.value() << endl;//error
    }
};
只可以在类S中声明类T的指针及引用,不可以声明对象,也不可以通过声明的指针及引用去调用类T的成员!因为编译器需要知道类T的详细具体的信息,此时你应该包含类T的头文件!

6.显示调用类的析构函数:

T t; t.T::~T();或 T* t; t->T::~T();如果t是placement new出来的,则可以显示调用类的析构函数;如果是普通的new,则应用delete/delete[];如果t是对象,则编译器会自动调用其析构函数,你不要显示调用,否则你会死的很惨!


7.template class的成员函数也是template function,它只在被调用时才实例化,并且不随class一起实例化。

8.在类T的成员函数中可以访问类T的:public/protected/private成员,也可以访问类T的基类的public/protected成员,不可以访问基类的private成员。

9.friendship不会被继承。一个函数是类T的基类的friend,但它不是类T的friend,除非显示地在T中声明。

10.friend function可以在类中定义,但它不是类的成员函数。此时,不可像定义在global scope中一样可以直接调用该函数,也不可以用class scope来限定调用它,因为它不是类的成员,自然也不在class scope中。可以用ADL(argument-dependent lookup)查找规则来调用该函数,如:

class T
{
    friend void fr(){ cout << ”i am your friend” << endl;}
    friend void fr( T t ){ cout << “i am your friend, t” << endl;}
};

int main()
{
    fr();//error
    
    T t;
    fr( t );//ok,because of ADL.根据ADL规则编译器会到参数t的类型T所在的namespace和class T scope中去找“候选”函数
}

11. 子类构造函数的执行顺序为:先父类,再客人,最后自己。具体顺序为:父类构造函数的执行按继承的顺序,如:class subclass : public A,public B则先执行A的构造函数,然后执行B的构造函数;客人为子类中声明的成员对象,它们的构造函数执行顺序按在子类中声明的顺序为准;最后执行子类自己的构造函数体。注意:类的析构顺序与类的构造顺序正相反:先自己,后客人,再父类,具体顺序也是如此。

12.模板类(template class)其内的成员函数也都是template function,所以与普通的template function一样,这些template function的定义应该放在头文件.hxx中,与类定义放在一起,最好不要放在实现文件.cxx中,因为有些编译器还不支持这种包含模式。

13.抽象类就是有成员函数声明为纯虚函数的类,它不能被实例化!如:

class T
{
    void f()=0;//只有虚函数可以被赋予0
    virtual void print()=0;//ok.print为纯虚函数。
    virtual void value()=0//ok. value为纯虚函数,纯虚函数也可以有定义
    {
        cout << “T::value()” << endl;
    }
    
};

class S : public T
{
    void print(){ cout << “S::print()” << endl;}
    void value(){ cout << ”T::value()” << endl;}//即使基类T中已经实现了value()但子类S也必须实现自己的value(),否则S为为一个抽象类,不能被实例化。
};

int main()
{
    T t; //error
    S s; //ok

    s.value();//输出: S:value()
    s.T::value(); //输出:T:value()
}

14.避免构造或析构函数中调用虚函数
    如果你已经从另外一种语言如c#或者java转向了c++,你会觉得,避免在类的构造函数或者析构函数中调用虚函数这一原则有点违背直觉。但是在c++中,违反这个原则会给你带来难以预料的后果和无尽的烦恼。
  我想以重复本文的主题开篇:不要在类的构造或者析构函数中调用虚函数,因为这种调用不会如你所愿,即使成功一点,最后还会使你沮丧不已。如果你以前是一个java或者c#程序员,请密切注意本节的内容-这正是c++与其它语言的大区别之一。

  假设你有一个为股票交易建模的类层次结构,例如买单,卖单,等等。为该类交易建立审计系统是非常重要的,这样的话,每当创建一个交易对象,在审计登录项上就生成一个适当的入口项。这看上去不失为一种解决该问题的合理方法:
class transaction {// 所有交易的基类
public:
 transaction();
 virtual void logtransaction() const = 0;//建立依赖于具体交易类型的登录项
 ...
};
transaction::transaction() //实现基类的构造函数
{
 ...
 logtransaction(); //最后,登录该交易
}
class buytransaction: public transaction {
// 派生类
public:
 virtual void logtransaction() const; //怎样实现这种类型交易的登录?
 ...
};
class selltransaction: public transaction {
//派生类
public:
 virtual void logtransaction() const; //怎样实现这种类型交易的登录?
 ...
};

  现在,请分析执行下列代码调用时所发生的事情:
buytransaction b;

  很明显,一个 buytransaction类构造器被调用。但是,首先调用的是transaction类的构造器-派生类对象的基类部分是在派生类部分之前被构造的。 transaction构造器的最后一行调用了虚函数logtransaction,但是奇怪的事情正是在此发生的。被调用函数 logtransaction的版本是transaction中的那个,而不是buytransaction中的那个-即使现在产生的对象的类型是 buytransaction,情况也是如此。在基类的构造过程中,虚函数调用从不会被传递到派生类中。代之的是,派生类对象表现出来的行为好象其本身就是基类型。不规范地说,在基类的构造过程中,虚函数并没有被"构造"。

  对上面这种看上去有点违背直觉的行为可以用一个理由来解释-因为基类构造器是在派生类之前执行的,所以在基类构造器运行的时候派生类的数据成员还没有被初始化。如果在基类的构造过程中对虚函数的调用传递到了派生类,派生类对象当然可以参照引用局部的数据成员,但是这些数据成员其时尚未被初始化。这将会导致无休止的未定义行为和彻夜的代码调试。沿类层次往下调用尚未初始化的对象的某些部分本来就是危险的,所以c++干脆不让你这样做。

  事实上还有比这更具基本的要求。在派生类对象的基类对象构造过程中,该类的类型是基类类型。不仅虚函数依赖于基类,而且使用运行时刻信息的语言的相应部分(例如,dynamic_cast(参见item 27)和typeid)也把该对象当基类类型对待。在我们的示例中,当transaction的构造器正运行以初始化buytransaction对象的基类部分时,该对象是transaction类型。在c++编程中处处都这样处理,这样做很有意义:在基类对象的初始化中,派生类对象 buytransaction相关部分并未被初始化,所以其时把这些部分当作根本不存在是最安全的。在一个派生类对象的构造器开始执行之前,它不会成为一个派生类对象的。

  在对象的析构期间,存在与上面同样的逻辑。一旦一个派生类的析构器运行起来,该对象的派生类数据成员就被假设为是未定义的值,这样以来,c++就把它们当做是不存在一样。一旦进入到基类的析构器中,该对象即变为一个基类对象,c++中各个部分(虚函数,dynamic_cast运算符等等)都这样处理。

  在上面的示例代码中,transaction构造器直接调用了一个虚函数-这明显地破坏了本文所强调的原则。这种破坏性非常容易觉察,一些编译器对此发出警告(注意:另外一些编译器并不给出警告,请参考item 53有关警告的讨论)。即使没有给出警告,该问题在代码运行时刻也是相当明显的,因为函数logtransaction是类transaction中的纯虚函数。除非该函数被定义了(可能性不太大,但确实存在这种情况-参见item 34),否则程序不会进行链接:链接器没法找到transaction::logtransaction的必需的实现代码。

  在类的构造或者析构函数中进行虚函数调用并非总是那么容易被发现。如果transaction类有多个构造器且其中每个必须执行一些相同的任务,也许只有优秀的软件工程师才能够避免代码的重复,这可以通过把相同的初始化代码(包括调用logtransaction)放到一个私有的且非虚的初始化函数中实现,譬如下面的init:
class transaction {
 public:
  transaction()
  { init(); } //调用非虚函数...
  virtual void logtransaction() const = 0;
  ...
 private:
  void init()
  {
   ...
   logtransaction(); //注意这里调用了虚函数
  }
};

  这段代码从概念上看与前面的版本一样,但是却更具有潜在的危险性,因为典型情况下,该代码会被成功地编译与链接。在这种情况下,因为 logtransaction是transaction类中的纯虚函数,绝大多数的运行时刻系统会在该纯虚函数被调用时(典型地是通过发送一个带有调用该函数意义的消息实现)流产掉程序。然而,如果logtransaction是一个"正常的"虚函数(也就是,不是纯虚的),并在transaction中有它的实现部分,该代码段将被调用而且程序会顺利地运行一段时间,这让你考虑为什么在一个派生类对象被创建时调用了logtransaction的错误版本。唯一避免该问题的办法是确保没有任何一个构造器或者析构器在正被产生或毁坏的对象上调用了虚函数,而且所有其调用的函数都要遵循同样的约束。

  但是,每当有一个对象在transaction类层次结构中产生时,如何保证调用的是logtransaction的正确版本呢?很明显,从transaction的构造器中调用对象上的虚函数是错误的做法。

  有几种不同的办法可以解决这个问题。一种办法就是在transaction中把函数logtransaction改变为一个非虚函数,然后要求派生子类的构造器要把必要的登录信息传递给transaction的构造器。如此以来,上面的函数就能够安全地调用非虚函数logtransaction了。如下所示:
class transaction {
 public:
  explicit transaction(const std::string& loginfo);
  void logtransaction(const std::string& loginfo) const;//现在是一个非虚函数
  ...
};

transaction::transaction(const std::string& loginfo)
{
 ...
 logtransaction(loginfo);// 现在调用的是一个非虚函数
}

class buytransaction: public transaction {
 public:
  buytransaction( parameters )
  :transaction(createlogstring(parameters)) { ... } //把登录信息传送给基类的构造函数
  ...
 private:
  static std::string createlogstring( parameters );
};

  换句话说,既然在基类的构造函数中不能沿着类的继承层次往下调用虚函数,你可以通过在派生类中沿着类的层次结构把必要的构造信息传递到基类的构造器中来补偿这一点。

  在这个例子中,请注意buytransaction中私有静态函数createlogstring的使用方法。通过使用帮助函数来创建一个值并把它传递到基类构造器中,这种方式比起在成员初始化列表中实现基类所需的操作要更方便和更具有可读性。这里我们把该函数创建为static型,这对于偶尔参照引用一下刚产生的buytransaction对象的尚未初始化的数据成员是没有危险的。这一点很重要,因为那些数据成员还处于一种未定义的状态中,这一事实解释了为什么在基类的构造或者析构函数中对于虚函数的调用不能首先传递到派生子类中去。
  结论:
  不要在类的构造或者析构过程中调用虚函数,因为这样的调用永远不会沿类继承树往下传递到子类中去。

15.构造函数是不可以声明为虚函数的,否则编译器报错,因为对象被彻底创建出来之前,是没办法多态的

16.基类的析构函数应该总是声明为虚函数,避免资源泄露。如:

#include

class T
{
    public:

        T(){ std::cout << "T create" << std::endl;}
       virtual ~T(){ std::cout << "T destroy" << std::endl;}
};

class Son : public T
{
    public:
    Son(){ std::cout << "Son create" << std::endl;}
    ~Son(){ std::cout << "Son destroy" << std::endl;}
};

int main()
{
    
    T* pson = new Son;
    delete pson; //如果类T的析构函数不是虚函数,则析构时,只有基类T的析构函数被调用,而子类Son的析构函数不会被调用!

    return 0;
}

17.继承的访问控制:
    当子类以public方式继承时:父类的public/protected部份的成员在子类中依然是public/protected的,只是基类的 private部份对于其子类是不可见,不可访问的;当子类以private方式继承时:父类的public/protected部分转变为子类中的 private部份,直接子类可以访问,但是对子类的子类就是不可见,不可访问的,即,儿子可以用,孙子不可用,而父类中的private在其子类中依然是不可见的,不可访问的!当子类以protected方式继承时:父类的public/protected成为子类的protected部分,而父类的 private对于子类而言还是不可见,不可访问的!说一点哲学上的事:西方人认为孩子与父母是分开的独立实体,谁也没有权力干扰对方的private;而东方人认为孩子是父母不可分割的联系体,你即我,我即你!没有private!

18.当子类以protected/private方式继承时,指向子类的指针不能转化为指向父类的指针,否则编译器报错。与其说这是继承,不如说这是另一种组合方式,如:

class Base
{
public:
    int x;
protected:
    int y;


};

class Son1 : private Base
{
};

class Son2 : protected Base
{
};

class Son3 : public Base
{
};

int main()
{
    Son1* pson1 = new Son1;
    Son2* pson2 = new Son2;
    Son3* pson3 = new Son3;

    Base* pbase1 = static_cast(pson1); //error
    Base* pbase2 = static_cast(pson2); //error
    Base* pbase3 = static_cast(pson3);//ok

    return 0;
}

19.当子类以protected/private方式继承时,可以用using将父类的public/protected成员在子类中开放,注意:父类的private成员无法开放给子类,即使用using也不行。如:

class T
{
public:
    void publicF(){}
protected:
    void protectedF(){}
};

class S : private T
{
public:
    using T::publicF;//publicF在基类T中为public,则在子类S中也只能是public的
protectd:
    using T::protectedF;//protectedF在基类T中为protected,则在子类S中也只能是protected的
};
这样通过S的对象就可以访问T::publicF,T::protectedF了。


20。多继承--共同基类:

    Base
/        
F1        F2
        /
    D
这个类D对象的组成中有两个Base对象,那么调用Base的方法时总会出现“二义调用”:D::F1::Base::f和D::F2::Base::f,解决方法:多重虚继承,即:

#include

class Base
{
public:
    Base(){ std::cout << "Base Cteate!" << std::endl;}

    void printBase(){ std::cout << "printBase()" << std::endl;}
};

class F1 : public  virtual  Base
{

};

class F2 : public  virtual  Base
{
};

class D : public  F1, public  F2
{
};

int main()
{
    D d;

    d.printBase();

    return 0;
}


21.关于类的组合型数据成员:

    (1)如果该数据成员是类的每个实例私有的,且可能各不相同,则声明成值对象(composite by value).
    (2)如果该成员数据是类的多个对象共有的或者是个外部对象,则声明为指针或引用(composite by pointer or by reference )

22. typeid(express) 或typeid(type) 其返回值为type_info类型对象,type_info的复制构造函数和赋值操作符皆是私有的。
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值