C++类学习

一、C++类的定义
    C++中使用关键字 class 来定义类, 其基本形式如下:
class 类名
{
public:
protected:
//行为或属性
private:
//行为或属性
};
 
示例:
     定义一个点(Point)类, 具有以下属性和方法:
     ■ 属性: x坐标, y坐标
     ■ 方法: 1.设置x,y的坐标值; 2.输出坐标的信息。
实现代码:
class Point
{
public:
     void setPoint(int x, int y);
     void printPoint();
 
private:
     int xPos;
     int yPos;
};  
代码说明:
     上段代码中定义了一个名为 Point 的类, 具有两个私密属性, int型的xPos和yPos, 分别用来表示x点和y点。
     在方法上, setPoint 用来设置属性, 也就是 xPos 和 yPos 的值; printPoint 用来输出点的信息。    

1 数据抽象和封装
     抽象是通过特定的实例抽取共同特征以后形成概念的过程。一个对象是现实世界中一个实体的抽象,一个类是一组对象的抽象。
     封装是将相关的概念组成一个单元,然后通过一个名称来引用它。面向对象封装是将数据和基于数据的操作封装成一个整体对象,对数据的访问或修改只能通过对象对外提供的接口进行。
 
2 类定义
     几个重要名词:
(1) 类名
     遵循一般的命名规则; 字母,数字和下划线组合,不要以数字开头。
(2) 类成员
     类可以没有成员,也可以定义多个成员。成员可以是数据、函数或类型别名。所有的成员都必须在类的内部声明。
     没有成员的类是空类,空类也占用空间。
class People
{
};
sizeof(People) = 1;    
(3) 构造函数
     构造函数是一个特殊的、与类同名的成员函数,用于给每个数据成员设置适当的初始值。
(4) 成员函数
     成员函数必须在类内部声明,可以在类内部定义,也可以在类外部定义。如果在类内部定义,就默认是内联函数。
 
3 类定义补充
3.1 可使用类型别名来简化类
     除了定义数据和函数成员之外,类还可以定义自己的局部类型名字。
     使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。
class People
public: 
     typedef std::string phonenum//电话号码类型
 
     phonenum phonePub; //公开号码
private:      
     phonenum phonePri;//私人号码
}; 
 
3.2 成员函数可被重载
     可以有多个重载成员函数,个数不限。
3.3 内联函数
     有三种:
(1)直接在类内部定义。
(2)在类内部声明,加上inline关键字,在类外部定义。
(3)在类内部声明,在类外部定义,同时加上inline关键字。注意:此种情况下,内联函数的定义通常应该放在类定义的同一头文件中,而不是在源文件中。这是为了保证内联函数的定义在调用该函数的每个源文件中是可见的。
3.4 访问限制
     public,private,protected 为属性/方法限制的关键字。
3.5 类的数据成员中不能使用 auto、extern和register等进行修饰, 也不能在定义时进行初始化
     如 int xPos = 0; //错;
例外:
          静态常量整型(包括char,bool)数据成员可以直接在类的定义体中进行初始化,例如:
          static const int ia= 30; 
 
4 类声明与类定义
4.1 类声明(declare)
class Screen;
      在声明之后,定义之前,只知道Screen是一个类名,但不知道包含哪些成员。只能以有限方式使用它,不能定义该类型的对象,只能用于定义指向该类型的指针或引用,声明(不是定义)使用该类型作为形参类型或返回类型的函数。
void Test1(Screen& a){};
void Test1(Screen* a){};
4.2 类定义(define)
     在创建类的对象之前,必须完整的定义该类,而不只是声明类。所以,类不能具有自身类型的数据成员,但可以包含指向本类的指针或引用。
class LinkScreen
{
public:
          Screen window;
          LinkScreen* next;
          LinkScreen* prev;
}; //注意,分号不能丢
     因为在类定义之后可以接一个对象定义列表,可类比内置类型,定义必须以分号结束:
class LinkScreen{ /* ... */ };
class LinkScreen{ /* ... */ } scr1,scr2; 
     
5 类对象
     定义类对象时,将为其分配存储空间。
     Sales_item item; //编译器分配了足以容纳一个 Sales_item 对象的存储空间。item 指的就是那个存储空间。
 
6 隐含的 this 指针 
     成员函数具有一个附加的隐含形参,即 this指针,它由编译器隐含地定义。成员函数的函数体可以显式使用 this 指针。
6.1 何时使用 this 指针
     当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用 this:该函数返回对调用该函数的对象的引用。
class Screen 
{
...
public:
      Screen& set(char);
};
Screen& Screen::set(char c) 
{
      contents[cursor] = c;
      return *this;
}

7 类作用域
     每个类都定义了自己的作用域和唯一的类型。
     类的作用域包括:类的内部(花括号之内), 定义在类外部的成员函数的参数表(小括号之内)和函数体(花括号之内)。
class Screen 
//类的内部
...
}; 
//类的外部
char Screen::get(index r, index c) const
{
     index row = r * width;      // compute the row location
     return contents[row + c];   // offset by c to fetch specified character

     注意:成员函数的返回类型不一定在类作用域中。可通过 类名::来判断是否是类的作用域,::之前不属于类的作用域,::之后属于类的作用域。例如
Screen:: 之前的返回类型就不在类的作用域,Screen:: 之后的函数名开始到函数体都是类的作用域。
class Screen 
public: 
     typedef std::string::size_type index; 
     index get_cursor() const; 
}; 
Screen::index Screen::get_cursor() const   //注意:index前面的Screen不能少
     return cursor; 
     该函数的返回类型是 index,这是在 Screen 类内部定义的一个类型名。在类作用域之外使用,必须用完全限定的类型名 Screen::index 来指定所需要的 index 是在类 Screen 中定义的名字。
 
 
二 构造函数
     构造函数是特殊的成员函数,用来保证每个对象的数据成员具有合适的初始值。
     构造函数名字与类名相同,不能指定返回类型(也不能定义返回类型为void),可以有0-n个形参。
     在创建类的对象时,编译器就运行一个构造函数。
 
1 构造函数可以重载
     可以为一个类声明的构造函数的数量没有限制,只要每个构造函数的形参表是唯一的。
class Sales_item;
{
public: 
     Sales_item(const std::string&); 
     Sales_item(std::istream&); 
     Sales_item(); //默认构造函数
}; 
 
2 构造函数自动执行 
     只要创建该类型的一个对象,编译器就运行一个构造函数:
Sales_item item1("0-201-54848-8");
Sales_item *p = new Sales_item(); 
     第一种情况下,运行接受一个 string 实参的构造函数,来初始化变量item1。
     第二种情况下,动态分配一个新的 Sales_item 对象,通过运行默认构造函数初始化该对象。
 
3 构造函数初始化式
     与其他函数一样,构造函数具有名字、形参表和函数体。
     与其他函数不同的是,构造函数可以包含一个构造函数初始化列表:  
Sales_item::Sales_item(const string &book): isbn(book), units_sold(0), revenue(0.0)
{ } 
     构造函数初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。
     构造函数可以定义在类的内部或外部。构造函数初始化只在构造函数的定义中指定。
     构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。初始化列表属于初始化阶段(1),构造函数函数体中的所有语句属于计算阶段(2)。
     初始化列表比构造函数体先执行。不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。
3.1 哪种类需要初始化式
     const 对象或引用类型的对象,可以初始化,但不能对它们赋值,而且在开始执行构造函数的函数体之前要完成初始化。
     初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中,在构造函数函数体中对它们赋值不起作用。
     没有默认构造函数的类类型的成员,以及 const 或引用类型的成员,必须在初始化列表中完成初始化。
class ConstRef 
public: 
     ConstRef(int ii); 
private: 
     int i; 
     const int ci; 
     int &ri; 
}; 
ConstRef::ConstRef(int ii) 
{
     i = ii;   // ok 
     ci = ii;  // error
     ri = i;   // 
}
     应该这么初始化:
ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { } 
3.2 成员初始化的次序
     每个成员在构造函数初始化列表中只能指定一次。重复初始化,编译器一般会有提示。
     成员被初始化的次序就是定义成员的次序,跟初始化列表中的顺序无关。
3.3 初始化式表达式
     初始化式可以是任意表达式
Sales_item(const std::string &book, int cnt, double price): isbn(book), units_sold(cnt), revenue(cnt * price) { }
3.4 类类型的数据成员的初始化式
     初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数,可以使用该类型的任意构造函数。
Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {}
 3.5 类对象的数据成员的初始化      
     在类A的构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化。
     类类型的数据成员,运行该类型的默认构造函数来初始化。
     内置或复合类型的成员的初始值依赖于该类对象的作用域:在局部作用域中不被初始化,在全局作用域中被初始化为0。假设有一个类A,
class A
{
    public:
        int ia;
        B b;
};
    A类对象A a;不管a在局部作用域还是全局作用域,b使用B类的默认构造函数来初始化,ia的初始化取决于a的作用域,a在局部作用域,ia不被初始化,a在全局作用域,ia初始化0。

4 默认构造函数 
     不含形参的构造函数就是默认构造函数。     
     只要定义一个对象时没有提供初始化式,就使用默认构造函数。如: A a;
     为所有形参提供默认实参的构造函数也定义了默认构造函数。例如:
class A
{
public: 
     A(int a=1,char c =''){}
private:  
     int ia;
     char c1;
};
4.1 合成的默认构造函数
     只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。
     一个类只要定义了一个构造函数,编译器也不会再生成默认构造函数。
建议:
     如果定义了其他构造函数,也提供一个默认构造函数。
     如果类包含内置或复合类型(如 int& 或 string*)的成员,它应该定义自己的构造函数来初始化这些成员。每个构造函数应该为每个内置或复合类型的成员提供初始化。
 
5 隐式类类型转换
5.1 只含单个形参的构造函数能够实现从形参类型到该类类型的一个隐式转换
class A
{
public:
     A(int a)
     {
          ia =a;
     }
 
     bool EqualTo(const A& a)
     {
          return ia == a.ia;
     }
 
private:
     int ia;
};
 
A a(1);
bool bEq = false;
bEq = a.EqualTo(1);//参数为1,实现从int型到A的隐式转换
 
5.2抑制由构造函数定义的隐式转换
     通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数: 
class A
{
public:
     explicit A(int a )
     {
          ia =a;
     }
 
     bool EqualTo(const A& a)
     {
          return ia == a.ia;
     }
 
private:
     int ia;
};
     通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。将构造函数设置为 explicit 可以避免错误。
 
 
三 复制控制
1 复制构造函数
1.1 几个要点
(1) 复制构造函数
     复制构造函数是一种特殊构造函数,只有1个形参,该形参(常用 const &修饰)是对该类类型的引用。
class Peopel
{
public:
     Peopel();//默认构造函数
     Peopel(const Peopel&);//复制构造函数
     ~Peopel();//析构函数
};
     当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数。
Peopel a1; Peopel a2 = a1;
     当将该类型的对象传递给函数或函数返回该类型的对象时,将隐式使用复制构造函数。
Peopel Func(Peopel b){...}
(2)析构函数
     析构函数是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。
     析构函数可用于释放构造对象时或在对象的生命期中所获取的资源。
     不管类是否定义了自己的析构函数,编译器都自动执行类中非 static 数据成员的析构函数。
(3) 复制控制
     复制构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。
(4) 两种初始化形式
     C++ 支持两种初始化形式:直接初始化和复制初始化。直接初始化将初始化式放在圆括号中,复制初始化使用 = 符号。
     对于内置类型,例如int, double等,直接初始化和复制初始化没有区别。
     对于类类型:直接初始化直接调用与实参匹配的构造函数;复制初始化先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。直接初始化比复制初始化更快。
(5)形参和返回值
     当形参或返回值为类类型时,由该类的复制构造函数进行复制。 
(6)初始化容器元素
     复制构造函数可用于初始化顺序容器中的元素。例如:
vector<string> svec(5);
     编译器首先使用 string 默认构造函数创建一个临时值,然后使用复制构造函数将临时值复制到 svec 的每个元素。 
(7)构造函数与数组元素
     如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素。
     如果使用常规的花括号括住的数组初始化列表来提供显式元素初始化式,则使用复制初始化来初始化每个元素。根据指定值创建适当类型的元素,然后用复制构造函数将该值复制到相应元素:
Sales_item primer_eds[] = { string("0-201-16487-6"),
                                 string("0-201-54848-8"),
                                 string("0-201-82470-1"),
                                 Sales_item()
                               };

1.2 合成的复制构造函数
(1)合成的复制构造函数
     如果没有定义复制构造函数,编译器就会为我们合成一个。
     合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本。
逐个成员初始化:合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。
例外:如果一个类具有数组成员,则合成复制构造函数将复制数组。复制数组时合成复制构造函数将复制数组的每一个元素。

1.3 定义自己的复制构造函数
(1) 只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数,也可以复制。 
class Peopel
{
public:
     std::string name;
     unsigned int id;
     unsigned int age;
     std::string address;
};
(2) 有些类必须对复制对象时发生的事情加以控制。
     例如,类有一个数据成员是指针,或者有成员表示在构造函数中分配的其他资源。而另一些类在创建新对象时必须做一些特定工作。这两种情况下,都必须定义自己的复制构造函数。
     最好显式或隐式定义默认构造函数和复制构造函数。如果定义了复制构造函数,必须定义默认构造函数。
 
1.4 禁止复制
     有些类需要完全禁止复制。例如,iostream 类就不允许复制。延伸:容器内元素不能为iostream 
     为了防止复制,类必须显式声明其复制构造函数为 private。

2 赋值操作符
     与复制构造函数一样,如果类没有定义自己的赋值操作符,则编译器会合成一个。
(1)重载赋值操作符
Sales_item& operator=(const Sales_item &);
(2)合成赋值操作符
     合成赋值操作符会逐个成员赋值:右操作数对象的每个成员赋值给左操作数对象的对应成员。除数组之外,每个成员用所属类型的常规方式进行赋值。对于数组,给每个数组元素赋值。
(3)复制和赋值常一起使用 
     一般而言,如果类需要复制构造函数,它也会需要赋值操作符。 

3 析构函数
     构造函数的用途之一是自动获取资源;与之相对的是,析构函数的用途之一是回收资源。除此之外,析构函数可以执行任意类设计者希望在该类对象的使用完毕之后执行的操作。
(1) 何时调用析构函数
  • 撤销(销毁)类对象时会自动调用析构函数。
  • 变量(类对象)在超出作用域时应该自动撤销(销毁)。
  • 动态分配的对象(new A)只有在指向该对象的指针被删除时才撤销(销毁)。
  • 撤销(销毁)一个容器(不管是标准库容器还是内置数组)时,也会运行容器中的类类型元素的析构函数(容器中的元素总是从后往前撤销)。
(2)何时编写显式析构函数
     如果类需要定义析构函数,则它也需要定义赋值操作符和复制构造函数,这个规则常称为三法则:如果类需要析构函数,则需要所有这三个复制控制成员。
(3)合成析构函数
     合成析构函数按对象创建时的逆序撤销每个非 static 成员,因此,它按成员在类中声明次序的逆序撤销成员。
     对于每个类类型的成员,合成析构函数调用该成员的析构函数来撤销对象。
     合成析构函数并不删除指针成员所指向的对象。 所以,如果有指针成员,一定要定义自己的析构函数来删除指针。

     析构函数与复制构造函数或赋值操作符之间的一个重要区别:即使我们编写了自己的析构函数,合成析构函数仍然运行。
 
 
四 友元
     友元机制允许一个类将对其非公有成员的访问权授予指定的函数
     友元可以出现在类定义的内部的任何地方。
     友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。
     建议:将友元声明成组地放在类定义的开始或结尾
 
1 友元类
class Husband
{
public:
     friend class Wife;
private:
     double money;//钱是老公私有的,别人不能动,但老婆除外
};
 
class Wife
{
public:
     void Consume(Husband& h)
     {
          h.money -= 10000;//老婆可以花老公的钱
     }
};
 
Husband h;
Wife w;
w.Consume(h);
 
2 使其他类的成员函数成为友元
class Husband; //1.声明Husband 
 
class Wife //2.定义Wife类 
{
public:
     void Consume(Husband& h);
};
 
class Husband //3.定义Husband类
{
public:
     friend void Wife::Consume(Husband& h);//声明Consume函数。
private:
     double money;//钱是老公私有的,别人不能动,但老婆除外
};
 
void Wife::Consume(Husband& h) //4.定义Consume函数。
{
     h.money -= 10000;//老婆可以花老公的钱
}
注意类和函数的声明和定义的顺序:
(1)声明类Husband 
(2)定义类Wife,声明Consume函数
(3)定义类Husband
(4)定义Consume函数。
 
 
五 static 类成员
 
static 成员,有全局对象的作用,但又不破坏封装。
1 static 成员变量
static 数据成员是与类关联的对象,并不与该类的对象相关联。
static 成员遵循正常的公有/私有访问规则。  

2 使用 static 成员而不是全局对象有三个优点。
(1)  static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
(2)  可以实施封装。static 成员可以是私有成员,而全局对象不可以。
(3)  通过阅读程序容易看出 static 成员是与特定类关联的,这种可见性可清晰地显示程序员的意图。 

3 static 成员函数
     在类的内部声明函数时需要添加static关键字,但是在类外部定义函数时就不需要了。
     因为static 成员是类的组成部分但不是任何对象的组成部分,所以有以下几个特点:
1) static 函数没有 this 指针
2) static 成员函数不能被声明为 const (将成员函数声明为 const 就是承诺不会修改该函数所属的对象)
3) static 成员函数也不能被声明为虚函数

4 static 数据成员 
     static 数据成员可以声明为任意类型,可以是常量、引用、数组、类类型,等等。
     static 数据成员必须在类定义体的外部定义(正好一次),并且应该在定义时进行初始化。
建议:定义在类的源文件中名,即与类的非内联函数的定义同一个文件中。注意,定义时也要带上类类型+"::"
double Account::interestRate = 0.035; 

5 特殊的静态常量整型成员 
     静态常量整型数据成员可以直接在类的定义体中进行初始化,例如:
static const int period = 30; 
     当然char 可以转换成整形,也是可以的,   static const char bkground = '#';
 
6 其他
(1)static 数据成员的类型可以是该成员所属的类类型。非 static 成员只能是自身类对象的指针或引用 
class Screen 
{
public:
         // ...
private:
         static Screen src1; // ok
         Screen *src2;       // ok
         Screen src3;        // error
}; 
(2)非 static 数据成员不能用作默认实参,static 数据成员可用作默认实参
class Screen 
{
public:
          Screen& clear(char = bkground);
private:
         static const char bkground = '#';//static const整形变量可以在类内部初始化。
};
 


1.类的定义:
是用户自定义的数据类型。
C++一个类定义的形式如下:
class 类名 
{
        成员列表
};
成员列表是类成员的集合,数目可以任意多, 一对 { } 是成员列表边界符,与成员列表一起成为类体。类体后面必须用 ; 结束。
1.每个类可以没有成员,也可以有多个成员。
2.类成员可以是数据或函数。
3.所有成员必须在类内部声明,一旦类定义完成后,就没有任何其他方式可以再增加或减少成员。

在面向对象程序设计中,一般将变量(数据)隐蔽起来,外部不能直接访问。把成员函数作为对外界的接口,通过成员函数访问数据,可能一开始学习的时候不太理解,这个我们在后面会经常用到,请耐心观看。

类中如果有成员函数,则声明是必须的,而定义是可选的,什么意思呢,请看下例:

在类内部定义函数体
class 类名
{
         返回类型   函数名(形参列表)
         {
                  函数体
         }
};

在类外部定义函数体
class 类名
{
         返回类型   函数名(形参列表);
};
返回类型   类名 :: 函数名(形参列表)

         函数体

看到这里会产生一个问题,那就是这两种定义方法到底有什么区别,或者根本没有区别。
其实它们还是有区别的,类内部定义的函数,程序在要调用它的时候会把它当作是一个内联函数,内联函数的好处是调用速度更快,但是会占用额外的内存空间,每调用一次都相当于定义一次。而外部定义的函数,就不会被当作内联函数。对于一些要用到递归的函数,定义成内联函数肯定是不合理的。因此建议使用第二种方法定义成员函数。

类的定义一般放在程序文件开头,或者放到头文件中被程序文件包含,当然也可以放在局部作用域里。这里有必要提一下,c++规定,在局部作用域中声明的类,成员函数必须是函数定义形式,不能是原型声明。

类相当于一种新的数据类型,数据类型不占用存储空间,用类型定义一个实体的时候,才会为它分配存储空间。


2.类成员的访问控制:
对类的成员进行访问,有两个访问源:类成员和类用户。
类成员指类本身的成员函数,类用户指类外部的使用者,包括全局函数,另一个类的成员函数等。
在C++中,类的每个成员都有访问控制属性:public(公有的)、private(私有的)、protected(保护的)
类用户想要访问类的数据成员,必须通过公有成员访问。
我们上面说过,面向对象程序设计过程中一般将数据隐蔽起来,也就是说一般的变量(数据)都声明为private,而成员函数声明为public,protected在后面我们会用到,不考虑继承的话,和private的性质一致。如果在声明的时候不写访问控制属性,则类会默认它为private。

在刚才类定义的基础上进行扩展:
class 类名
{
public:
     公有的数据成员和成员函数
protected:
     保护的数据成员和成员函数
private:
     私有的数据成员和成员函数
};

类的成员函数和普通函数一样,也可以进行重载,设置默认参数,显式的指定为内联函数等。
这里有个小问题,请看下例:
class Test
{
public:
void Sum(int a=0,int b=0);
};
void Test::Sum(int a=0,int b=0)
{
cout<<a+b;
}
这是一个设置了默认参数的函数,但是很遗憾,这是错误的,下面这样则是正确的:
class Test
{
public:
void Sum(int a=0,int b=0);
};
void Test::Sum(int a,int b)
{
cout<<a+b;
}
原因是C++中对于特定的某个函数,设置默认形参这个动作只能有一次


3.对象的定义和使用:
说了这么多,怎么样才能实现在外部实现对类成员的访问呢?这就是我们要讨论的对象。

对类的定义就是定义了一个具体的数据类型,要使用它我们必须将类实例化,即定义该类的对象。
以下两种定义类对象的方法都是合法的(假定有一个Test类):
Test test1 , test2;
class Test test1 , test2;
我们之前说过,定义类型时不会分配存储空间,当定义一个对象的时候,将为其分配存储空间。

当然,有时候人们也希望可以动态的为其分配内存,当不用的时候再销毁它,就有了如下定义方式:
Test *p;
p = new Test;
当不再使用此动态对象的时候,必须用delete:
delete p;

现在我们关心的应该是怎么通过对象调用类的成员?
访问对象中的成员有三种方法:
通过对象名和对象成员引用运算符 (.) 
通过指向对象的指针和指针成员引用运算符 (->)
通过对象的引用变量和对象成员引用运算符 (.) 


假定我们有一个Test类,类中有一个公有的Sum()函数,则在外部调用Sum()的方法有:
Test test;
test.Sum();

Test *p;
p = new Test;
p->Sum();

Test test, &r = test;
r.Sum();
这些方式都是合法的。

 

4.构造函数与析构函数:
建立一个对象的时候,通常最需要做的工作就是初始化对象,如对数据成员赋初值,而构造函数就是用来在创建对象时初始化对象,为对象数据成员赋初值。为什么非得这么做呢?因为在类里面,数据成员不能够进行初始化。即:
class Test
{
int x = 0;
...
};
这样做是错误的。
想想为什么不可以,还是上面说过的,类只是定义了一个数据类型,不会占用存储空间,而在类里面对数据成员赋初值则会占用存储空间,因此自相矛盾。

如果数据成员是公有的,那么可以在类外直接对它初始化,但如果是私有的,那么就不能直接访问它,这就要用到构造函数。构造函数就是用来处理对象的初始化问题,构造函数是一种特殊的成员函数,不需要人为调用,而是在对象建立的时候自动被执行。

C++规定构造函数的名字要与类名保持一致,而且不能指定返回类型。请看下面程序:
#include <iostream>

using namespace std;

class Test
{
public:
Test ();
Test (int x,int y);
void Sum();
private:
int a,b;
};
Test::Test()
{

}
Test::Test(int x,int y)
{
a=x;
b=y;
}
void Test::Sum()
{
cout<<a+b;
}
int main()
{
Test test(3,4);
test.Sum();
return 0;
}
这里定义了两个构造函数 Test() 和 Test(int x,int y),由于创建对象一般是在类外部进行,因此构造函数声明为public。

第一个为无参构造函数或默认构造函数,写这个函数的好处是当你在创建对象的时候并不想立即对它初始化,而是在后续的工作中再进行赋初值,即:
Test test;
如果没有默认构造函数则会报错。那么问题来了,之前的例子我们根本没写构造函数,却不会报错,这是为什么?

因为在IDE里(ex:codeblocks)不会报错是因为IDE会自动生成一个默认构造函数。当然,如果你已经定义了一个有参的构造函数,它就不再为你自己生成一个默认构造函数,也就是说如果我们现在把这个Test类里的默认构造函数删除了,
Test test;
就会报错。


第二个构造函数就完成了我们的初始化工作,它有两个形参,分别给数据成员a,b进行初始化,定义对象的时候传入了 3和4,则 a和b 被初始化为 3 和 4 。因此程序运行的结果是 打印出了 7。

构造函数初始化列表

所谓初始化列表,它的功能和我们写在函数体里的赋初值是一样的,也就是说我们可以写成如下形式:

#include <iostream>

using namespace std;

class Test
{
public:
Test ();
Test (int x,int y);
void Sum();
private:
int a,b;
};
Test::Test()
{

}
Test::Test(int x,int y):a(x),b(y)
{

}
void Test::Sum()
{
cout<<a+b;
}
int main()
{
Test test(3,4);
test.Sum();
return 0;
}
你可以选择写的更简洁一点:
#include <iostream>

using namespace std;

class Test
{
public:
Test () {}
Test (int x,int y):a(x),b(y) {}
void Sum();
private:
int a,b;
};

void Test::Sum()
{
cout<<a+b;
}
int main()
{
Test test(3,4);
test.Sum();
return 0;
}
那么这样做和普通的赋值有区别吗?
当然是有的,对于一般的变量,两种都可行,但是如果需要初始化的是类类型的成员,则必须使用构造函数初始化列表。比如:

#include <iostream>

using namespace std;

class Test
{
public:
Test () {}
Test (int x,int y):a(x),b(y) {}
void Sum();
private:
int a,b;
};

void Test::Sum()
{
cout<<a+b;
}
class AnotherTest
{
public:
AnotherTest(int i,int j):test(i,j) {test.Sum();}
private:
Test test;
};
int main()
{
AnotherTest test(3,4);
return 0;
}

我们之前说过,类的成员函数可以重载,带默认参数等,那么构造函数呢?
构造函数也是可以的,刚才那个例子就是构造函数的重载,默认构造和有参构造。

下面是一个带默认参数的构造:
#include <iostream>

using namespace std;

class Test
{
public:
Test () {}
Test (int x = 0,int y = 0):a(x),b(y) {}
void Sum();
private:
int a,b;
};

void Test::Sum()
{
cout<<a+b;
}

int main()
{
Test test(3);
test.Sum();
return 0;
}
一旦指定了 x = 0,就必须指定 y 的值。

Test (int x = 0,int y):a(x),b(y) {}
这样是错误的。


复制构造函数

复制构造函数也称为拷贝构造函数,它的作用是用一个已经生成的对象来初始化另一个同类的对象。
即实现如下功能:
Test test1(3,4);
Test test2 = test1;
复制构造函数的写法:

类名 (const 类名& obj)
{
       函数体
}

例如:
#include <iostream>

using namespace std;

class Test
{
public:
Test () {}
Test (int x ,int y):a(x),b(y) {}
Test (const Test& t):a(t.a),b(t.b) {}
void Sum();
private:
int a,b;
};

void Test::Sum()
{
cout<<a+b;
}

int main()
{
Test test1(3,4);
Test test2 = test1;
test2.Sum();
return 0;
}
程序运行的结果是 7 ,它完成了给test2进行初始化。
当然也可以用如下语句:
Test test2(test1);
深复制和浅复制:

如果我们不定义复制构造函数,以上对象也可以这样进行初始化,原因就是系统也会自己生成一个复制构造函数。
现在存在这样一个类:
#include <iostream>
#include <cstring>

using namespace std;

class Test
{
public:
Test (int x,char *ptr)
{
a = x;
p = new char [x];
strcpy(p,ptr);
}
Test (const Test& C)
{
a = C.a;
p = new char [a];
p = C.p;
}
void Print();
private:
int a;
char *p;
};

void Test::Print()
{
int i = 0;
while(p[i] != '\0')
{
cout<<p[i];
i++;
}

}

int main()
{
char p[5] = "test";
Test a(10,p);
Test b(a);
b.Print();
return 0;
}


因为对象 a 和 b 指向的是同一段内存区域,如果我们在完成复制(浅复制)之后删除了 a,它指向的内存区域同样也被删除了,而此时 b 此时仍然指向的是这片区域,如果再把 b 删除掉,同一片内存区域被释放两次,这明显是错误的。也就是说,浅复制只是简单的将 a 中p 的值给了 b 中的 p。那么要解决这个问题就得用到深复制:
Test (const Test& C)
{
a = C.a;
p = new char [a];
if(p != 0)
strcpy(p,C.p);
}


析构函数:

析构函数在类里起了一个“清理”的作用,比如类中有需要动态开辟内存的成员,而在程序结束之后我们需要释放内存,这时我们只要将释放内存的语句写在析构函数中,而系统在程序运行结束之后会自动执行析构函数,进行内存的释放以及对象的销毁。
以下是一个例子:
#include <iostream>
#include <cstring>

using namespace std;

class Test
{
public:
Test (int x,char *ptr)
{
a = x;
p = new char [x];
strcpy(p,ptr);
}
Test (const Test& C)
{
a = C.a;
p = new char [a];
if(p != 0)
strcpy(p,C.p);
}
~Test()
{
delete (p);
cout<<"p has been destroyed"<<endl;
}
void Print();
private:
int a;
char *p;
};

void Test::Print()
{
int i = 0;
while(p[i] != '\0')
{
cout<<p[i];
i++;
}
cout<<endl;
}

int main()
{
char p[5] = "test";
Test a(10,p);
Test b(a);
b.Print();
return 0;
}
程序运行结果如下:


由此可见,a 和 b 的析构函数都被调用了。 

 

5.友元机制:
C++提供了友元机制,允许一个类将其非公有成员的访问权限授予指定的函数或类。友元的声明只能在类定义的内部,因此,访问类非公有成员除了自身成员,还有友元。
有如下程序:
#include <iostream>
#include <cstring>

using namespace std;

class Test
{
public:
Test (int a)
{
x = a;
}
~Test() //析构函数
{

}
friend void Print(Test& a,Test& b);
private:
int x;
};

void Print(Test& a,Test& b)
{
cout<<a.x*b.x;
}

int main()
{
Test a(10);
Test b(3);
Print(a,b);
return 0;
}
输出结果为 30 ,完成了求两个对象内的数据之积。

下面介绍友元类:
#include <iostream>
#include <cstring>

using namespace std;

class B; //类的前向声明
class A
{
public:
A(){}
~A() //析构函数
{

}
void Print(B& a);
};

class B
{
public:
B (int a)
{
x = a;
}
private:
int x;
friend void A::Print(B& a);
};
void A::Print(B& a)
{
cout<<a.x;
}
int main()
{
B test1(3);
A test2;
test2.Print(test1);
return 0;
}
 类A成功的访问了类B的私有成员,并且打印出来。输出结果为 3。

友元类的关系是单向的,即 A 是 B 的友元,B 不是 A 的友元,类 B 不能访问 A 的数据成员。此外,友元的关系不能传递或继承,类 B 是类 A 的友元,C 是 B 的友元,那么 C 不是 A 的友元,除非另外声明一次。

6.继承与派生:

继承是面向对象程序设计的一个重要特性,继承允许在原有类的基础上创建新的类,举个例子,现在有一个平行四边形类,而菱形类,矩形类,正方形类 都属于平行四边形类,它们有一个共同点,那就是需要两条边长来描述图形,如果不采用继承,我们需要在每个类中定义两个数据成员,那样会显得很繁琐。下面我们看一个计算矩形面积的例子:
#include <iostream>

using namespace std;

class Parallelogram
{
public:
Parallelogram(int a,int b):length(a),width(b) {}
int getLength(){return length;}
int getWidth() {return width;}
private:
int length,width;
};
class Rectangle : public Parallelogram //公有继承
{
public:
Rectangle(int a,int b):Parallelogram(a,b) {} //先对基类中的数据成员进行初始化
void Area() //计算面积
{
cout<<getLength()*getWidth();
}
};
int main()
{
Rectangle r(3,4);
r.Area();
return 0;
}
首先说一下继承方式,c++提供了三中继承方式。
public(公有继承),基类中的公有和保护成员保持原属性,私有成员为基类私有。
private(私有继承),基类中的所有成员在派生类中都是私有的。
protected(保护继承),基类的公有成员和保护成员在派生类中成了保护成员,私有成员仍为基类私有。

现在我们说如何设计一个派生类:
①从基类接收成员,除了构造函数和析构函数,派生类会把全部的成员继承过来,这是没有选择的。
②调整基类成员的访问。
③修改基类成员,可以在派生类中声明一个与基类同名的成员,此操作会覆盖基类的同名成员。
④在定义派生类的时候定义新的成员,定义构造函数和析构函数,初始化的时候必须先将基类的成员初始化(因为并没有继承基类的构造函数)之后才可以对派生类的成员进行初始化。析构函数也一样,需要在派生类中释放基类的数据成员(调用基类的析构函数)。

上面这个程序定义了一个基类(平行四边形类),它含有两个数据成员,代表两个边长,而矩形在计算面积的时候需要两个边长的长度,也就是长和宽,因此继承平行四边形类,并且新添加了计算面积的函数,程序输出结果 12。


多重继承和虚基类:
C++还支持一个派生类同时继承多个基类。
多重继承派生类的定义:

class  派生类名 : 访问标号1 基类名1 , 访问标号2 基类名2 , ....
{

         成员列表

}

同样,派生类的构造函数初始化列表在调用基类构造函数也应该按定义时的先后次序。

接下来我们看个例子:

#include <iostream>

using namespace std;

class BaseOne
{
public:
BaseOne() {cout<<"This is BaseOne"<<endl;}
BaseOne(int a):data(a) {cout<<"BaseOne's data is "<<data<<endl;}
private:
int data;
};

class BaseTwo
{
public:
BaseTwo() {cout<<"This is BaseTwo"<<endl;}
BaseTwo(int a):data(a) {cout<<"BaseTwo's data is "<<data<<endl;}
private:
int data;
};

class BaseThree
{
public:
BaseThree() {cout<<"This is BaseThree"<<endl;}
};

class Derive:public BaseOne,public BaseTwo,public BaseThree
{
public:
Derive () {cout<<"This is Derive"<<endl;}
Derive (int a,int b,int c,int d,int e):BaseOne(a),BaseTwo(b),dataOne(c),dataTwo(d),data(e)
{cout<<"Derive's data is "<<data<<endl;}
private:
BaseOne dataOne;
BaseTwo dataTwo;
int data;
};

int main()
{
Derive r1;
cout<<endl;
Derive r2(1,2,3,4,5);
return 0;
}

程序运行结果如下:

 

在调用派生类的默认构造函数时,即使没有写出调用基类的默认构造函数,系统也会调用基类的默认构造函数,而在结果的第4 , 5行还调用了一次,原因是派生类里有两个基类的数据成员,因此我们可以观察到,程序先调用了基类的构造函数,然后调用派生类中子对象的构造函数,最后调用派生类的构造函数。

在调用构造函数的时候,先调用基类的构造函数,虽然BaseThree没有参数,但是仍然会调用它的构造函数,然后初始化子对象,调用构造函数,最后调用派生类的构造函数。

 

二义性问题:

假定我们有如下程序:

#include <iostream>
#include <cstring>

using namespace std;

class A
{
public:
void fun() {cout<<"This is A"<<endl;}
};

class B
{
public:
void fun() {cout<<"This is B"<<endl;}
};

class C:public A,public B
{
public:
void hun() {fun();} //产生二义性
};

int main()
{
C c;
c.hun();
return 0;
}

相信看到这里大家都知道二义性问题的产生原因了吧,就是两个基类存在名称相同的数据成员,而派生类在调用基类数据成员的时候如果没有显式的指出它属于谁,那么程序就会产生错误,现在我们做如下修改:

void hun() {A::fun(); B::fun();}

这次程序会分别调用 A 和 B 的 fun() 函数。我们要做的只是在它前面写上 基类名加上域运算符 :: ,当然也可以通过 对象名.基类名 :: 和 对象指针名.基类名 :: 这两种方式。

 

虚基类:

假定我们现在有这样一种继承关系:

#include <iostream>

using namespace std;

class A
{
public:
void fun() {cout<<"This is A"<<endl;}
};

class B:public A
{
public:
void gun() {cout<<"This is B"<<endl;}
};

class C:public A
{
public:
void hun() {cout<<"This is C"<<endl;}
};

class D:public B,public C
{
public:
void kun() {fun();} //产生二义性
};

int main()
{
D d;
d.kun();
return 0;
}

A 是基类,B 是 A 的派生类,C 也是 A 的派生类,而 D 是 B 和 C 的派生类,因此 D 可以访问 A 的数据成员,但现在会产生二义性问题,我们必须显式的指出 fun() 是来自 B 的 还是来自 C 的,但是我们都知道它来自 A , 因此我们希望找到一种方式,使得在继承间接共同基类时只保留一份成员,这就用到了虚基类的机制。

虚基类是在派生类定义时,指定继承方式时声明的。声明的一般形式:

 

class  派生类名 : virtual  访问标号 虚基类名 , ...

{

        成员列表

 

为了保证虚基类在派生类中只继承一次,应当在所有直接派生类中声明为虚基类。依然是上面那个程序,我们只需要:

class B:virtual public A
class C:virtual public A

这样我们在类 D 中调用 fun() 函数,就不用指出它究竟属于谁。

 

接下来我们来看看虚基类构造函数和析构函数的一些特性。

有这样一个程序:

#include <iostream>

using namespace std;

class A
{
public:
A() {cout<<"This is Grandpa"<<endl;}
A(int a):One(a) {cout<<"Grandpa is "<<One<<" years old"<<endl;}
~A() {cout<<"A is over"<<endl;}
private:
int One;
};

class B:virtual public A
{
public:
B() {cout<<"This is father"<<endl;}
B(int a,int b):A(a),Two(b) {cout<<"father is "<<Two<<" years old"<<endl;}
~B() {cout<<"B is over"<<endl;}
private:
int Two;
};

class C:virtual public A
{
public:
C() {cout<<"This is mother"<<endl;}
C(int a,int b):A(a),Three(b) {cout<<"mother is "<<Three<<" years old"<<endl;}
~C() {cout<<"C is over"<<endl;}
private:
int Three;
};

class D:public B,public C
{
public:
D() {cout<<"This is me"<<endl;}
D(int a,int b,int c,int d):A(a),B(a,b),C(a,c),Four(d) {cout<<"I am "<<Four<<" years old"<<endl;}
~D() {cout<<"D is over"<<endl;}
private:
int Four;
};

int main()
{
D d1;
cout<<endl;
//D d2(65,40,39,13);
return 0;
}
首先看默认构造函数(先把d2注释掉),程序运行结果如下:

 

程序会自动调用构造函数,首先调用虚基类的构造函数,然后再根据派生类继承的次序调用构造函数,如果我们先继承了 C 类,那么先调用 C 类的构造函数。析构函数的调用则正好相反。

 

接下来看有参的,给 d1 加上注释,去掉 d2 的注释,程序运行结果如下:

 

调用次序是一样的,因此虚基类的构造函数优先于非虚基类的构造函数进行执行,如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有的派生类里(直接和间接)中,都必须通过构造函数的初始化列表对其进行初始化,在最后的派生类中不单单对直接继承的类进行初始化,还要对虚基类进行初始化。

 

7.多态性和虚函数:

首先介绍一下多态性,多态是指同样的消息被不同类型的对象接收时导致不同的行为,我们举个通俗易懂的例子,假定我们现在有一个模具,这个模具是一个人型模具,根据倒入里面金属液体的不同,它最终会形成不同类型的器件,如果倒入的是液体黄金,那么它会形成一个小金人,如果倒入的是铁水,那就会形成一个小铁人,多态大概就是这样的意思。

多态性可以通过很多方法实现,而我们要说的是 包含多态 实现多态性,C++采用虚函数实现包含多态,至少含有一个虚函数的类成为多态类。

在介绍虚函数之前,我们介绍两种联编。联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数分配内存地址,并且对外部访问也分配正确的内存地址。

在编译阶段就将函数实现和函数调用绑定起来称为静态联编,程序运行的时候才进行函数实现和函数调用的绑定称为动态联编。

我们举个例子:

#include <iostream>

using namespace std;

class A
{
public:
void fun() {cout<<"Use A"<<endl;}
};

class B : public A
{
public:
void fun() {cout<<"Use B"<<endl;}
};

int main()
{
B b;
A *p = &b;
p->fun();
return 0;
}

我们声明了一个指向类 B 的指针,但是程序的输出结果是:

 

之所以会这样是因为我们将其定义为 A 类型,程序在编译阶段就已经确定 A 类型的指针调用的 fun() 是 A 类的成员。

接下来我们来看动态联编:

给刚才类 A 的 fun() 函数前面加上 virtual ,

virtual void fun() {cout<<"Use A"<<endl;}
将其定义为了虚函数,再次运行:

 

当编译器编译含有虚函数的类时,将为它建立一个虚函数表,相当于一个指针数组,存放每个虚函数的入口地址,编译器为该类增加一个额外的数据成员,这个数据成员是一个指向虚函数表的指针,通常称为vptr。这个例子中,A 类有一个虚函数 fun() , 所以虚函数表里只有一项,如果派生类没有重写这个虚函数,那么虚函数表里的元素所指向的地址就是基类虚函数的地址,重写之后,则 vptr 指向派生类的虚函数地址。

派生类可以继承基类的虚函数表,而且只要和基类同名的成员函数,无论前面加不加 virtual ,都会自动成为虚函数,虚函数的调用规则是,根据当前对象,优先调用对象本身的成员函数。

 

虚析构函数:

如果将基类的析构函数声明为虚函数,那么其派生类的析构函数也变为虚析构函数,即使名字不同,当基类的析构函数是虚析构函数时,无论指针指的是同一类族中的哪一个类对象,系统总会采用动态联编,调用正确的析构函数,对该对象进行清理。C++中,不支持虚构造函数。

 

纯虚函数:

许多情况下,不能在基类中为虚函数给出一个有意义的定义,那就将其声明为纯虚函数,具体怎么实现交给派生类去做,纯虚函数的定义形式为:

 

virtual   返回类型  函数名 (形式参数列表) = 0;

 

纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对其进行定义,如果一个类里声明了虚函数,而在其派生类中没有对该函数定义,那么该函数在派生类中仍然为纯虚函数。含有纯虚函数的类成为抽象类,抽象类不能定义对象,如果派生类里给出了抽象类中纯虚函数的实现,那么该派生类不再是抽象类,否则仍然是抽象类。抽象类至少含有一个纯虚函数。

 

接下来我们看一个计算圆形面积和圆柱体体积的程序:

#include <iostream>

using namespace std;

class Sharp
{
public:
virtual double area() = 0;
virtual double volumn() = 0;
};

class Circle : public Sharp
{
public:
Circle(double r):R(r) {}
virtual double area() {return 3.1415926*R*R;}
virtual double volumn() {return 0;}
private:
double R;
};

class Cylinder : public Circle
{
public:
Cylinder(double a,double b):Circle(a),H(b) {}
virtual double volumn() {return area()*H;}
private:
double H;
};

int main()
{
Circle a(20.0);
Cylinder b(10.0,2.0);
cout<<a.area()<<endl;
cout<<b.volumn()<<endl;
return 0;
}

posted @ 2018-12-11 17:18  叕叒双又  阅读(270)  评论(0编辑  收藏  举报