1. 类和面向对象编程
在上一章节就讲过,即便是在面向过程的语言中,我们就遇到过类和对象的概念,
特别是在讲结构体时,类和对象的概念就更清晰了。

但是如果要完全按照面向对象的思想去开发程序,使用面向过程语言中的那些类
和简单的对象是无法满足要求的。

在c++中为了兼容c语言,c++中保留了结构体的数据类型,c++对结构体做了扩展
之后,完全可以使用结构体来实现面向对象的开发,但是c++中引入了另外一个关
键字class实现类定义,功能上几乎和结构体完全相同。



2. 面向对象编程的3特性
(1)封装
所谓封装就是通过某种段些,比如函数,比如结构体,类等手段将某些内容通通
写在这些结构里面,封装效果就是,对于外部来说,这些结构就相当于是黑匣子
,外面只需通过接口进行访问就可以了,不需要关心内部的情况。

当然说到封装就不分不提到有关隐藏的概念,所谓隐藏就是向外界完全屏蔽里面
的内容,在上一章讲结构体时,private和protected就是为了隐藏成员用的。


(2)继承
所谓继承就是通过已有基础类型扩展出新的类型,这样可以使得面向对象的程序
具有非常强大的生命力,在原有基础上可以扩展出非常多的新内容。

被扩展的就成为父类(基类),扩展出来的新类就被成为子类(派生类)。   


(3)多态
相同的接口可以根据实际不同的情况执行不同内容的函数(方法),从而得到不
同的结果。

在c语言中是通过向公共层的同一个函数指针变量注册下层函数地址,然后上层
就可以根据这个公共的函数指针去回掉下层的函数,如果如果下层注册的是A函
数,那么调用得到A执行的结果,如果注册的是B函数,那么得到的就是B函数执
行的结果,具体是哪一个结果,就要有具体注册时决定。

在c中,往往会把各种下层的函数指针统一打包到一个结构体变量中,向上层注
册时,直接注册结构体变量。

在面向对象的语言中则是通过向上转型和函数重写的方式来实现多态的,尽管在
具体的实现手段上有所区别,但是在多态的实现逻辑上是完全一致的。

有了封装,继承,多态,面向对象的语言很容按照分层的思想的去构建大型层序,越是底
层的语言,实现分层就越是困难,越是上层的语言,实现分层思想设计和实现程序就越是
容易。c语言实现分层的难易程度属于中等。

前面强调过,c语言也可以实现面向对象的程序开发,但是c面向对象开发时是跛脚的,因
为c中无法实现继承。

3.定义一个类并实例化对象
定义一个类与定义一个结构体的形式几乎一样,所不同的是,定义类时使用的是
class关键字。使用类类型定义一个类类型的变量叫做实例化,这个变量又被叫
做对象。


(1)实例化对象并{ }初始化 
    (1)实例化对象的两种方式
        (1)像定义普通变量一样去定义
        (2)使用指针和new进行动态定义   

(2)使用{ }去初始化对象
    对于普通方式可以直接使用{ }像初始化结构体一样初始化类对象。但是有四种情况
    不能直接使用{ }进行初始化:

    (1)情况1:如果成员为private或者protected的话,就不能直接对其进行初始化。

        由于类中的成员默认就是private,为了能够向初始化结构体一样初始化它,
        我们需要将成员变为public。


    (2)情况2:如果包含类类型的成员的话,不能使用{ }进行初始化
        面对这种情况我们必须使用类的构造函数进行初始化。

    (3)情况3:如果类中有显式定义构造函数的话,无法使用{ }进行初始化     

    (4)情况4:动态分配方式实例化对象时,无法使用{ }方式初始化

例子: 

class Student
{
public:
        int age;
        double money;
        char gender;
        char name[30];

    void showme();
}

void Student::showme()
{
        std::cout << name << std::endl;
        std::cout << "性别" << gender <<  std::endl;
        std::cout << age << "岁" << std::endl;
        std::cout << money << "¥" << std::endl;
}

Student stu1 = {20, 30000, 'M', "zhangsan"};

int main(int argc, char **argv)
{
        Student stu2 = {20, 30000, 'M', "wangwu"};      

        Student *stu3 = new Student();                  

        stu1.showme();
        stu2.showme();
        stu3->showme();

        delete stu3;

        return 0;
}



例子分析:
stu1和stu2都采用了普通方式,stu3采用了指针方式实例化对象,但是stu1是
一个全局变量,因此对象空间开辟于静态区,因为有初始化,所以是开辟于静
态区的.data区。而stu2的对象空间则开辟与main函数栈中,stu3比较特殊。
空间分为了两部分,stu3的指针空间开辟与main函数栈中,但是其指向的对象
空间则开辟于堆空间中。

从本例可以看出,在c++中开辟对象空间的方式比较多样化,但是在java中,所
有的对象空间的开辟方式只有一种,都采用了动态开辟的方式(开辟于堆中)。

在本例子中,完全可以将函数定义在类.的内部,但是绝大多数做法都是将函数直
接写在.cpp中,然后再将声明写到类里面。

本例子中的对象stu的初始化完全采用了结构体的初始化模式,在这里是可行的。       


4. 构造函数
(1)构造函数作用
    在使用类实例化一个对象时,构造函数会被调用用于初始化对象成员。为什么需
    要使用构造函数来初始化对象呢?因为当对象包含类类型的成员时是无法使用{ }
    这种初始化方式的。

在实际开发中,我们为了统一操作,基本都是用构造函数初始化对象。


(2)构造函数的特点
    (1)构造函数没有返回值,所以给构造函数制定返回值是错误的。
    (2)构造函数名要求与类名同名。
    (3)构造函数只是用于初始化,不是用于开辟对象空间。


(3)this指针
    this存放了当前对象的地址,在类内部访问成员使用this指针,一般情况省略,
    但是当类内部定义的函数的形参名与类内部的成员名同名,需要在类的内部函
    数里面访问同名的成员时,需要使用this指针显式的访问成员。


(4)有关构造函数需要注意的地方
    (1)如果不显式的定义构造函数,编译器会给配一个隐式的默认构造函数,类
        型为
        类名(){

    }

    Student stu;
    Student stu = Student();
    Student *stu = new Student();
    会调用默认构造函数。

    调用默认的无参初始化函数时。


(2)如果显式的定义了构造函数,显式的构造函数就会盖掉默认的无参构造函数。
    如果显式定义的构造函数构是有参的,那么如下
        Student stu;
        Student stu = Student();
        Student *stu = new Student();

    将会无法调用无参的构造函数,要么给参数调用带参数构造函数,要么
    显示定义无参构造函数,这里建议,为了使类具有良好的可移植性,最
    好都显示定义上好无参构造函数。


(3)构造函数的位置
    构造函数可以写在类的内部,也可以写在外部,写在外需要加类名::
    修饰,但是很多时候我们都是将构造函数写在外部,因为一个正规类
    的构造函数可能会非常大。


(4)构造函数可以被重载
    不清楚重载知识的,请回看讲函数的章节。

(5)例子
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <iostream>
    #include <string>
    #include <cassert>

    class Student
    {
        public:
            int age;
            double money;
            char gender;
            char name[30];

            Student(int age, double money, char gender, const char name[]);

            void showme();
    };

    Student::Student(int age, double money, char gender, const char name[])
    {
            this->age = age;
            this->money = money;
            this->gender = gender;
            strcpy(this->name, name);
    }

    void Student::showme()
    {
            std::cout << name << std::endl;
            std::cout << "性别" << gender <<  std::endl;
            std::cout << age << "岁" << std::endl;
            std::cout << money << "¥" << std::endl << std::endl;
        std::cout << "---------------------" << std::endl;
    }

    //Student stu1 = {20, 30000, 'M', "wangwu"};  
    int main(int argc, char **argv)
    {     
            //Student stu2;
            //Student *stu3 = new Student();

            Student stu4(20, 30000, 'M', "wangwu");
            Student *stu5 = new Student(20, 30000, 'M', "wangwu");

            stu4.showme();
            stu5->showme();

            delete(stu5);

            return 0;
    }

    例子分析:
        //Student stu1 = {20, 30000, 'M', "wangwu"};    
    如果打开这句话,将无法编译通过。因为类有显式定义构造函数,所以无法
    在使用{ }括号的方式进行初始化。

        //Student stu2;
    如果将这句话打开,也无法通过编译,因为默认的无参构造函数已经不存在了。
    如果希望通过编译,那么就在在类里面显式的重载一个无参构造函数
    Student(){ }。

        //Student *stu3 = new Student();
    这句话打开后也无法通过编译,因为这里的new Student()希望调用无
    参构造函数,如果希望通过编译的话,可以在类里面显式的重载无参构
    造函数Student(){ }。

        Student stu4(20, 30000, 'M', "wangwu");
        Student *stu5 = new Student(20, 30000, 'M', "wangwu");
        只有以上这两句话会被正确编译执行。


(5)调用默认的无参构造函数时,成员的值会被初始化为多少
    由于无参构造函数并没有去初始化对象成员,所以对象成员的初始值
    就是空间之前遗留的值,具体是多少决于被实例化的对象空间开辟于内存
    的什么位置。

    (1)开辟静态区的.bss:空间会被自动清0

    (2)开辟于堆(heap):空间会被自动清0

    (3)开辟于栈(stack):为栈空间的上一次使用者使用后遗留值(随机值)       

    例子:

    以上例子为例,如果我们我们在类里面重载上默认的无参构造函数,
        Student(){ }
    然后再将西面三句话打开,
        //Student stu1 = {20, 30000, 'M', "wangwu"};   
            //Student stu2;
            //Student *stu3 = new Student();
    并在main函数的后面再加上下面三句话。
            stu1.showme();

            stu2.showme();
            stu3->showme();


    编译运行后:
    stu1.showme();打印的结果为:
        性别
        0岁
        0¥
    打印的结果为空的,因为stu1空间开辟静态去.bss中。


    stu2.showme();打印的结果为:
        @??
        性别

        4313892岁
        -0.0204021¥
    打印的结果为随机值,因为stu1空间开辟于main函数栈中。      



    stu2.showme();打印的结果为:

        性别
        0岁
        0¥

    打印的结果为空,因为stu1空间开辟于堆中。  



(5) 给构造函数的形参指定默认的初始化值
    (1)可以给构造函数的形参制定初始值

讲函数的时候说过,可以给函数的形参指定默认的初始值,如果调用函数时,不
指定实参的话,就会使用默认的指定值。

指定默认初始值对于构造函数来说同样管用。

如果构造函数只有定义,没有声明,那么指定的默认值就应该写在定义里面,如果
有声明,就应该写在函数声明里面。

比如下面三个构造函数的重载:
Student(int age=1, double money=2, char gender='q', const char name[]="w") {
    this->age = age;
        this->money = money;
        this->gender = gender;
        strcpy(this->name, name);
} 
Student(double money, char gender='q', const char name[]="w"){
        this->money = money;
        this->gender = gender;
        strcpy(this->name, name);   
}
Student(char gender, const char name[]="w") { 
        this->gender = gender;
        strcpy(this->name, name);
}   

以上三个都成立,参数列表必须不同,实际上还可以根据参数列表的顺序来区分重载,
比如下面会被认为是两种不同的情况:
Student(char gender, const char name[]="w") {......} 
Student(const char name[]="w", char gender) {......} 


(2)对于默认构造函数要注意
    (1)如果构造函数的形参全部指定了默认值,这个构造函数会被定为是默认构造函数
    (2)类中只允许一个默认构造函数

如下三个重载的构造函数将无法通过编译:
    student() { }
    Student(int age=1, double money=2, char gender='q', const char name[]="w") {
        this->age = age;
            this->money = money;
            this->gender = gender;
            strcpy(this->name, name);
    } 
    Student(double money=233, char gender='q', const char name[]="w"){
        this->money = money;
            this->gender = gender;
            strcpy(this->name, name);
    }

因为这三个都可以作为默认构造函数,所以我们只能取其中一个。


(6)在构造函数中初始化数据成员

(1)在构造函数中使用赋值语句初始化成员

    还是以前面Student的构造函数为例,它的构造函数原本为:
    Student(int age=1, double money=2, char gender='q', const char name[]="w") {
        this->age = age;
            this->money = money;
            this->gender = gender;
            strcpy(this->name, name);
    } 

    实际上还有另外一种可行的初始化方式,那就是使用初始化列表,比如
    上面的例子完全可以改为下面的形式:
    Student(int age=1, double money=2, char gender='q', const char name[]="w") 
        :age(age), money(money), gender(gender)
    {
            strcpy(this->name, name);
    } 

    上面没有加上name(name)的原因是,因为name是c样式的char *形式的字符串数组没,有办法
    使用name(name)方式的初始化列表,但是如果将char *name的定义改为std::string就可以。

    上面的构造函数就改为:
    Student(int age=1, double money=2, char gender='q', const string name("w")) 
        :age(age), money(money), gender(gender), name(name)
    {
    } 


(2)初始化列表的个数可以省略,顺序可以颠倒  
    Student(int age=1, double money=2, char gender='q', const string name("w")) 
        :age(age), money(money), gender(gender), name(name)
    {  

     }

    Student(int age=1, double money=2, char gender='q', const string name("w")) 
        :age(age), money(money), name(name)
    {  

     }

    Student(int age=1, double money=2, char gender='q', const string name("w")) 
        :age(age), money(money), name(name),gender(gender),
    {  

     }
    以上三种都可以。    


(3)两种初始化方式的区别
    (1)在构造函数里面赋值的方式
        先创建数据成员,然后调用赋值语句给成员赋值,本质上时赋值。


    (2)使用初始化列表的方式
        创建数据成员时直接初始化,相比赋值效率更高,本质上是初始化。

(7)对类类型成员定义需要注意

(1)包含了类型成员需要注意
    (1)类中允许包含类类型的对象成员,不能包含自己类型的对象   
        与结构体一样,为了防止出现无止尽的递归形式的初始化。

    (2)但是可以包含自己类型的指针。

(2)举例
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <iostream>
    #include <string>
    #include <cassert>

    using namespace std;

    class Birthday
    {
    public:
            string year;
            string month;
            string day;

            Birthday(const string year="2008", const string month="12", const string day="12")
                    :year(year), month(month), day(day)
            {
            }

            void showdate()
            {
                    cout << "year:" << year << endl;
                    cout << "month:" << month << endl;
                    cout << "day:"  << day << endl;
             }
    };

    class Student
    {
            int age;
            double money;
            char gender;
            string name;
            Birthday birthday;

    public:
            Student(int age=1, double money=2, char gender='q', const string name="w",
                    string year="2007", string month="12", string day="12");

            void showme();
    };
    Student::Student(int age, double money, char gender, const string name,
            string year, string month, string day)
    {
            this->age = age;
            this->money = money;
            this->gender = gender;
            this->name = name;

            this->birthday.year = year;
            this->birthday.month = month;
            this->birthday.day = day;
    }

    void Student::showme()
    {
            std::cout << "我是" << name << std::endl;
            std::cout << "性别" << gender <<  std::endl;
            std::cout << age << "岁" << std::endl;
            std::cout << money << "¥" << std::endl << std::endl;
            birthday.showdate();

            std::cout << "---------------------" << std::endl;
    }

    int main(int argc, char **argv)
    {
            Student stu1;
            stu1.showme();

            return 0;
    }

    例子分析:
    本例子中Student的构造函数中,由于
            this->birthday.year = year;
            this->birthday.month = month;
            this->birthday.day = day;
    执行以上赋值操作时,要求Birthday中year,month,day成员必须是public的。
    但是如果我们使用初始化列表的话,可以调用Birthday的构造函数初始化它的year,
    month,day这三个成员,将这三个修饰为private或者protected也没有关系。

    构造函数可以改为如下样子:
    Student::Student(int age, double money, char gender, const string name,
            string year, string month, string day):
            age(age), money(money), gender(gender), name(name), birthday(year, month, day)
    {

    }

    在本例中,请注意Birthday的实现。

(3)如果将成员birthday改为指针的话
    Student的构造函数应该有如下两种方式。
    方式1:
    Student::Student(int age, double money, char gender, const string name, 
            string year, string month, string day):birthday(new Birthday(year, month,day))
    {
            this->age = age;
            this->money = money;
            this->gender = gender;
            this->name = name;
            //birthday = new Birthday();//这里也可以        
    }

    方式2:
    Student::Student(int age, double money, char gender, const string name,
        string year, string month, string day):
        age(age), money(money), gender(gender), name(name), birthday(new Birthday(year, month,day))
    {
            //birthday = new Birthday(year, month, day);//在这里也可以
    }


(8)使用explicit关键字
    (1)c++中如果构造函数只有一个参数的话,存在一定的隐患,因为可能会把传递参数隐式转为
        该类的类类型,这样的情况在某些时候会导致意想不到的错误。

    比如下面这个例子:
    #include <iostream>
    #include <string>
    #include <cassert>

    using namespace std;

    class Cub
    {
            int side;

        public:
            Cub(int side): side(side) { }

            int caculate_volume() {
                    return side *side*side;
            }

            bool compare(Cub cub) {
                    return this->side >= cub.side;
            }
    };

    int main(int argc, char **argv)
    {
            Cub cub1(3);
            Cub cub2(3);

            /* 以下这段代码会正常运行 */
            if(cub1.compare(cub2))
                    cout << "cub1' volume >= cub2' volume" << endl;
            else cout << "cub1' volume < cub2' volume" << endl;

            /* 27会被隐式转换为Cub 类型,转为Cub类型时,27会被当作边长
            *  但是实际本例子的真实想法是想把27当作体积 */
            if(cub1.compare(27))
                    cout << "cub1' volume >= cub2' volume" << endl;
            else cout << "cub1' volume < cub2' volume" << endl;

            return 0;
    }

    例子分析:
    本例子中如下这段代码会正常运行,
        if(cub1.compare(cub2))
                    cout << "cub1' volume >= cub2' volume" << endl;
            else cout << "cub1' volume < cub2' volume" << endl;


    但是如下这段代码是有一定歧义的,
            if(cub1.compare(27))
                    cout << "cub1' volume >= cub2' volume" << endl;
            else cout << "cub1' volume < cub2' volume" << endl;

    按照原本还以,希望27是作为体积值被使用,但是实际情况是27会被隐式
    转换为Cub类型,这个时候,27就被当做了边长被使用。



(2)使用explicit关键字,可以避免隐士式转换带来的歧义问题
    将例子中的构造函数加explicit修饰,改为如下形式,
        explicit Cub(int side): side(side) { }
    当编译器编译时会报错误,提示涉及隐式转换。


5. 类的私有成员(隐藏成员)
(1)隐藏数据成员

(1)隐藏的好处
(1)隐藏的最大好处就是可以降低耦合,耦合越少,越容易开发出更高效的大型程序。
(2)我们不应将类里面过多的细节暴露出来
    (1)防止用户恶意操作
    (2)想用户屏蔽哪些复杂的事情,尽量只将最傻瓜最简单最稳定的的东西暴露出来

比如下面这个例子:
#include <iostream>
#include <string>
#include <cassert>

using namespace std;
class date
{       
public:
        string year;
        string month;
        string day

        Cub(const string year, const string month, const string day)
                : year(year), month(month), day(day) { }
};

int main(int argc, char **argv)
{
        return 0;
}

例子分析:
本例子中,将date中的年月日天定义为了public,如果这三个时间是非常重要的
时间的话,非常容易遭受外来访问者的修改。时间格式都是有一定的要求的,如
果完全暴露给用户,用户可能会设置非法时间格式,比如设置一个2月30日,但是
2月显然是没有30号的。

除此外,如果用户可以直接访问这些日期成员的话,那么无疑加重了用户的使用
难度,应为用户需要知道class几乎所有成员的作用。而且还非常不利于解除耦合
,在调用者大量的访问中year/month/day成员时,如果定义class类的人后期将
year改为YEAT,month改为MONTH,dayt改为DAY,这时候调用者就会需要去频繁的
修改程序,显然这是不合理的。

基于以上描述,应该将不希望暴露的成员都设置为隐藏,防止外界直接访问。


(2)设置器和获取器

前面描述到,类中的数据成员都应该设置为私有或者受到保护的,但是这样时外
界如何与之发生联系呢,当然如果是初始化的话,我们完全可以通过构造函数进
行,这只是初始化,如果我们希望单个的去修改某个数据成员或者获取某个数据
成员的值得时候应该怎么办呢?我们需要加入设置器和获取器这样的函数接口。
而且在设置其中我们还可以对数据进行有效性检查。

设置器和获取器也叫setter和getter,这里借用了java中的称法。

将上面的例子改进后如下:
    #include <iostream>
    #include <string>
    #include <cassert>

    using namespace std;

    class Date
    {    
            int year;
            int month;
            int day;

    public:
            Date(int year=0, int month=0, int day=0)
                    : year(year), month(month), day(day) { } 

            /* setter */
            void set_year(int year) {
                    if(year > 0) {
                            this->year = year; 
                    } else cout << "年份要求>0" << endl;
            }   
            void set_month(int month) {
                    if(month>0 && month<=12) {
                            this->month = month;
                    } else cout << "月份要求1~12之间" << endl;
            }
            void set_day(int day) {
                    if(day>0 && day<=30) {
                           this->day = day;
                    } else cout << "月日要求1~30之间" << endl;
            }

            /* getter */
            int get_year() {
                    return year;
            }
            int get_month() {
                    return month;
            }
            int get_day() {
                    return day;
            }
    };

    int main(int argc, char **argv)
    {
            Date date;

            cout << "设置日期" << endl<<endl;
            date.set_year(1);
            date.set_month(12);
            date.set_day(24);

            cout << "请设置日期" << endl;
            cout << date.get_year() << endl;
            cout << date.get_month() << endl;
            cout << date.get_day() << endl;

            return 0;
    }


(2)默认的副本构造函数(也叫拷贝构造函数)

(1)调用默认副本构造函数
前面的例子为例,如果我们
    Date date1(2010, 23, 43);

如果我们还希望定义另一个与date1内容完全一致的date2的话,我们可以再定义一个。
    Date date2(2010, 23, 43);

但是我们实际上还可以有另外的一种当时来实现,那就使用类自带的默认构造函数复
制一个,使用格式如下。
    Date date2(date1);

这句话会自动的调用类的默认的副本构造函数从date1中复制一个date2出来,而且
内容完全一致。

这里使用的是默认的副本构造函数,如果当我们的类中包含类类型指针形式的的成
员时,会涉及一个深浅拷贝的问题,所谓浅拷贝就是拷贝出来的对象里面的指针成
员和被拷贝的的指针成员指向了同一个空间,而深拷贝不是,会指向不同的空间,
并且把内容复制到新空间里面去。

当需要深拷贝时,默认的副本构造函数显然就不够用了,我们需要显式实现自己的
副本构造函数。


请看下面还是关于日期的例子:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <iostream>
    #include <string>
    #include <cassert>

    using namespace std;

    class Date
    {
            int year;
            int month;
            int day;
            char *wealth;

        public:
            Date(int year=0, int month=0, int day=0, const char *wealth="小雨")
                    : year(year), month(month), day(day) {
                    this->wealth = (char *)malloc(strlen(wealth));
                    strcpy(this->wealth, wealth);
            }

            void set_wealth(const char *wealth) {
                    strcpy(this->wealth, wealth);

            }

            char *get_wealth() {
                    return wealth;
            }
    };

    int main(int argc, char **argv)
    {
            Date date1(2009, 20,12, "晴");
            Date date2(date1);

            cout << "date1 天气:" << date1.get_wealth() << endl;
            cout << "date2 天气:" << date2.get_wealth() << endl;

            cout << "date2 对天气修改后" << endl;
            date2.set_wealth("多云");
            cout << "date1 天气:" << date1.get_wealth() << endl;
            cout << "date2 天气:" << date2.get_wealth() << endl;

            return 0;
    }


    打印结果:
        date1 天气:晴
        date2 天气:晴
        date2 对天气修改后
        date1 天气:多云
        date2 天气:多云


    例子分析:
    本例中,下面两句话调用默认副本构造函数实现了date2的赋值,             
        Date date1(2009, 20,12, "晴");
            Date date2(date1);
    所以当调用相面两句打印语句时,你会发现date1和date2的内容是一致的
            cout << "date1 天气:" << date1.get_wealth() << endl;
            cout << "date2 天气:" << date2.get_wealth() << endl;

    但是当执行了下面这句话后,将date2的天气改为多云以后,
            date2.set_wealth("多云");
    调用打印语句打印后,我们会惊讶的发现,date1的天气居然也被修改为了
    多云,导致原因就是默认的副本构造函数在复制date1的wealth成员时,只
    是复制了wealth的存放的地址,因此导致date1和date2的wealth指定了同
    一个空间,如果我们不是为了实现有意思的空间的共享的话,这样的情况
    往往是很危险的,因为它只实现了浅拷贝。



(2)显式的实现自己的副本构造函数
在前面的例子中我们显然看到了默认的副本构造函数值只能实现浅拷贝,但是浅拷
往往有很多的隐患,特别是当类包含类类型的指针时,浅拷贝是不行的。因此我们
需要自己显式在类中实现自己的副本构造函数。

还是接着上面的例子进行讲解,在上面例子中的Date类中加入如下副本构造函数,
注意要放在public的下面。
        /* 自定义副本构造函数 */
        Date(Date &date) {
                this->year = date.year;
                this->month = date.month;
                this->day = date.day;
                this->wealth = (char *)malloc(strlen(date.wealth));
                strcpy(this->wealth, wealth);
        }

然后编译运行,结果为:
    date1 天气:晴
    date2 天气:晴
    date2 对天气修改后
    date1 天气:晴
    date2 天气:多云

通过以上结果你会发现,date1和date2实现了完全的分离。


 (3)副本构造函数的形参为什么是引用
副本构造函数的形参使用的是引用,这里必须使用引用,如果不写成引用,而是写
成如下形式的话。
    Date(Date date) {......}

这里需要将实参复制给形参,这个时候会再次调用副本构造函数,再次调用副本构造
函数时又涉及复制问题,又要调用副本构造函数,这将会导致无限递归,显然是不允
许的。如果写成这样,编译器是无法编译通过的。

解决这一问题的办法就是,将形参写为引用。


6. 友元
(1)什么是友元
    当我们将类的数据成员设置为隐藏时,通常情况下外部的函数是无法访问它们的,
    当然这么做的目的就是为了保护成员,但是在c++有一类外部函数有殊荣可以访问
    类的隐藏成员,这一类的外部函数被称为某个类的友元。

设置友元的主要情况就是,有一类的外部函数与某个类有着非常亲密的关系,
涉及频繁访问该类的成员的时候,我们就非常有必要将该函数设置为该类的友元。

前面说到外部函数有机会成为某个类的友元,实际上类内的成员函数也有机会可
以成为另一个的友元,这种情况时。比如A类的成员函数希望成为B类的友元,那
么我们只需要将A类设置为B类的友元类即可。

所以友元有两种:
(1)友元函数
(2)友元类

友元声明与访问权限没有关联,因此放在任何位置都可以。


(2)友元函数
    请看下例:
    #include <iostream>
    #include <string>
    #include <cassert>

using namespace std;

class Date
{    
        int year;
        int month;
        int day;
        char *wealth;
        friend void fun();

public:
        Date(int year=0, int month=0, int day=0, const char *wealth="小雨")
                : year(year), month(month), day(day) {
                this->wealth = (char *)malloc(strlen(wealth));
                strcpy(this->wealth, wealth);
        } 
};

void fun(){
        Date date1(2009, 20,12, "晴");
        cout << "date1 year:" << date1.year << endl;
        cout << "date1 month:" << date1.month << endl;
        cout << "date1 day:" << date1.day << endl;
}

int main(void)
{
        fun();

        return 0;
}

例子分析:
本例中,Date的数据成员都是隐藏的,原本fun函数是没有机会直接通过
“date.成员”的方式去访问的,但是由于在Date类中使用
    friend void fun();
将fun声明为了Date的友元,因此外部函数fun就可以直接访问Date的数据成员了。

(3)友元类

友元类例子:
    #include <iostream>
    #include <string>
    #include <cassert>

    using namespace std;

    class Date
    {
            int year;
            int month;
            int day;
            char *wealth;
            friend class Friendclass;

    public:
            Date(int year=0, int month=0, int day=0, const char *wealth="小雨")
                    : year(year), month(month), day(day) {
                    this->wealth = (char *)malloc(strlen(wealth));
                    strcpy(this->wealth, wealth);
            }
    };


    class Friendclass {

    public:
            void fun(Date &date) {
                    cout << "date year:" << date.year << endl;
                    cout << "date month:" << date.month << endl;
                    cout << "date day:" << date.day << endl;
            }
    };

    int main(void)
    {
            Date date(2009, 20,12, "晴");
            Friendclass friendclass;

            friendclass.fun(date);

            return 0;
    }

    例子分析:
    在本例中,Date的数据成员都是隐藏的,原本Friendclass类的fun成员
    函数是没有机会通过date.year在外部访问Date的数据成员的,但是由于
    我们将Friendclass类设置为了Date的友元类,因此Friendclass类的fun
    成员函数就可以在外部直接通过.成员符号访问Date的成员。


7. const
(1)const修饰形参
    (1)当我们传参时,如果实参是常量,那么我们要求形参也必须使用const修饰。
        这实际上在讲函数的时候已经提到过了。

(2)当实参是引用或者指针时,但是调用的函数又不能修改实参时,我们也必须
    函数的形参改为const修饰。

(2)const对象与const函数
    (1)为什么需要定义const的对象
        比如:
            const Cub cub1; 
        cub1就是一个const对象,表示该对象的内容不能被修改。

    又比如下面这个成员函数:
            bool compare(const Cub &cub) const {
                    return this->volume() > cub.volume();
            }  


    在这个成员函数中,之所以将引用类型的形参定义为const,是想限制
    通过这个引用去修改对象的可能。     

(2)const类型的对象只能调用const的函数
    首先强调一点,const成员函数的特点就是,在该函数内部不能有修改
    成员数据的操作,否者将直接导致编译不不通过。

    为什么规定const对象只能调用const的成员函数呢,因为const类型对
    象的内容是不能被改变的,如果调用非const的成员函数的话,有改变
    const对象内容的危险。所以一旦出现const对象调用自己的非const成
    员函数时,会导致直接编译不通过。

    但是反过来非const的对象既可以调用非const的成员函数,也可以调用
    const成员函数。

    例子:
    #include <string.h>
    #include <iostream>
    #include <string>
    #include <cassert>

    using namespace std;

    class Cub
    {       
            int side;
    public:
            Cub(int side):side(side) { }

            int volume() const {
                    //side = 1000;
                    return side*side*side;
            }
    };

    int main(void)
    {
            Cub cub1(3);
            Cub cub2(2);

            cub1.compare(cub2);


            return 0;
    }

    例子分析:
    本例中主要是为了实现比较两个立方体的体积的大小,在本例子中,
    由于      
            bool compare(const Cub &cub) {
                    return this->volume() > cub.volume();
            }   
    函数的形参是const类型的引用,因此cub去调用volume()成员函数时,
    volume()函数必须是const修饰,在例子中确实是使用const进行了修饰
    ,const修饰函数时,需要将其放在参数列表的后面。

    如果我们希望在const修饰的volume()函数中进程任何修改成员的操作
    时,都将引起严重的编译错误。


(3)类的mutable数据成员    
    前面使用const修饰的成员函数时,有时候会出现一种矛盾,那就是const对象
    绝大多数时候都是不需要修改内容的,所以调用const的函数都是没有问题的,
    但是有少数情况是,const修饰的函数就是需要去修改某个特殊的数据成员,那
    么这个时候怎么办呢?

在这种情况下,只需要在该数据成员的前面加上mutable修饰即可。

例子:
    #include <string.h>
    #include <iostream>
    #include <string>
    #include <cassert>

    using namespace std;
    class Cub 
    {    
            mutable int side;
    public:
            Cub(int side=10):side(side) { } 

            int area() const {
                    side -= 1;
                    return side*side;
            }   
    };      

    例子分析:
    本例中,原本const修饰的area函数是无法修改成员side的,但是由于
    岁side使用mutable修饰,实际上就打破了不能修改的规则。



8. 类的对象数组
定义一个对象数组与定义普通的数组在定义形式上完全一样,特别是与定义结构体
数组一模一样,如果类不包含类类型成员,完全可以像初始化结构体数组一样使用
{ }去初始化类对象数组。

如果给每个数组中的对象元素赋不同的值,可以使用使用设置器进行操作。

例子:
    #include <iostream>
    #include <string>
    #include <cassert>

    using namespace std;
    class Cub
    {
            int side;
    public:

            Cub(int side=1):side(side) { }

            void set_side(int side) {
                    cub[i] = Cub(i);
            }

            int get_side() {
                    return side;
            }   
    }; 

    int main(void)
    {
            Cub cub[5];
        Cub *cubp[5];

            for(int i=0; i<5; i++)
            {
                    cub[i] = Cub(i);
            cubp[i] = new Cub(2*i);

            }

            for(int i=0; i<5; i++) cout << cub[i].get_side() << " ";
            cout << endl;
            for(int i=0; i<5; i++) cout << cubp[i]->get_side() << " ";
            cout << endl; 

            return 0;
    }



9. 类对象的大小
类对象的大小总是>=器成员空间大小的总和,这与计算结构体变量的空间大小
是一样的情况,这都是因为结构体变量和对象在内存中存储时,需要对齐存放
导致的。

所以有关对象的对齐存放请参之前讲解的结构体对齐存放的内容。



10. 类的静态成员

类的静态成员分成了两种,一种是静态数据成员,一种是静态成员函数,静态数据成员与
函数的静态局部变量实现原理非常相似,都是开辟于静态空间。但是类的静态成员函数
有其特殊的含义和目的。

静态成员需要使用static进行修饰。


(1)类的静态数据成员
    (1)静态数据成员与普通成员的区别
        (1)静态成员空间开辟于静态存储区,普通成员的空间开辟于何处要看
            对象空间开辟于何处。

    (2)某个类实例化的所有对象都拥有独立的普通成员,但是所有对象的静态
        成员是共用。

    (3)静态成员独立于对象而存在,在没有实例化任何对象的时候,静态
        成员就已经存在。

    (4)静态数据如何定义?
        static int num;

    (5)定义了静态数据成员后,必须经过初始化后才能使用,否则编译一
        定会报错,而且必须将初始化操作的放在全局位置,不能放在函
        数和类内进行,否者无法通过编译。

        在初始化变量时不能加static关键字,并且需要使用”类名::”

        int Cub::num=0;

        静态数据成员不能通过构造函数的初始化列表去初始化,但是可以
        构造函数里面对其进行赋值。

    (8)静态成员的访问
        (1)静态成员可以被类直接访问,访问方式为
                类名::静态成员

            直接使用类名访问静态成员时,不依赖于对象的存在。
            这种情况需要将其设置为public。

        (2)可以被对象使用.或者->访问
            需要实例化对象并将其设置为public。

        (3)可以被普通成员函数访问,但是使用普通成员访问的前提是对
            象必须存在,普通成员函数依赖于对象而存在。

        (4)也可以静态成员函数访问,被静态成员函数访问的情况在后面
            讲静态成员函数时再涉及。

    (9)静态数据成员有什么作用
        (1)利用所有对象共享的静态成员,可以实现对象间的通信,
        (2)可以统计某个类实例化的对象个数


(2)例子:使用静态数据成员统计实例化对象的个数    

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <iostream>
    #include <string>
    #include <cassert>

    using namespace std;
    class Cub
    {
    public:
            int side;
    static  int num;
            Cub(int side=1):side(side) { 
            num++;
        }

            int get_num() {
                    return num;
            }   
    };     

    int Cub::num = 0;

    int main(void)
    {
            Cub *cubp[5] = {NULL};  
            Cub *cubp[5] = {NULL};

            printf("----------使用类名访问静态数据成员----------\n");
            for(int i=0; i<5; i++)
                    cout << "通过类名Cub::" << i << "访问num,查看实例化了多少个对象" << Cub::num << endl;

            for(int i=0; i<5; i++)
            {
                    cubp[i] = new Cub(i);
            }

            printf("\n----------使用对象访问静态数据成员----------\n");
            for(int i=0; i<5; i++)
                    cout << "通过对象" << i << "访问num,查看实例化了多少个对象" << cubp[i]->num << endl;

            printf("\n----------使用普通成员函数访问静态数据成员----------\n");
            for(int i=0; i<5; i++)
                    cout<<"通过对象"<<i<<"的普通成员函数get_num查看实例化了多少个对象" << cubp[i]->get_num() << endl;

            return 0;
    }

    运行结果:
        ----------使用类名访问静态数据成员----------
        通过类名Cub::0访问num,查看实例化了多少个对象0
        通过类名Cub::1访问num,查看实例化了多少个对象0
        通过类名Cub::2访问num,查看实例化了多少个对象0
        通过类名Cub::3访问num,查看实例化了多少个对象0
        通过类名Cub::4访问num,查看实例化了多少个对象0

        ----------使用对象访问静态数据成员----------
        通过对象0访问num,查看实例化了多少个对象5
        通过对象1访问num,查看实例化了多少个对象5
        通过对象2访问num,查看实例化了多少个对象5
        通过对象3访问num,查看实例化了多少个对象5
        通过对象4访问num,查看实例化了多少个对象5

        ----------使用普通成员函数访问静态数据成员----------
        通过对象0的普通成员函数get_num查看实例化了多少个对象5
        通过对象1的普通成员函数get_num查看实例化了多少个对象5
        通过对象2的普通成员函数get_num查看实例化了多少个对象5
        通过对象3的普通成员函数get_num查看实例化了多少个对象5
        通过对象4的普通成员函数get_num查看实例化了多少个对象5


    例子分析:

    定义了一个Cub类,这个类中定义了一个静态数据成员num,用于统计Cub
    类一共实例化了多少个具体的对象,其访问权限为public,num的初始化
    值为0。

    每创建一个对象,在构造函数里面就会对num进行++计数。

    不过本例子是通过对象指针实现的,在main函数中一开始定义了一个
    5个元素的Cub的指针数组,全部初始化为NULL了。

    紧接着通过如下语句
            printf("\n----------使用对象访问静态数据成员----------\n");
            for(int i=0; i<5; i++)
                    cout << "通过对象" << i << "查看实例化了多少个对象" << cubp[i]->num << endl;

    使用类名直接访问直接访问num的方式,将num的值打印出来,其num的打
    印的结果为0,也就说其值一直保持为其初始化的值,之所以是这个打印结果的原因就是,静态数据成员的存在不依赖于对象
    的是否存在,这里在使用类名访问静态数据成员时,这个时候还没有实例
    化任何对象,所以num没有进行++计数。


            但是在通过对象名和对象的成员函数get_num访问num时,这时已经实
    例化了5个对象,所以num的打印值都为5.                                        


(2)利用静态成员实现对象之间的通信
    #include <string.h>
    #include <iostream>
    #include <string>
    #include <cassert>

    using namespace std;
    class Family
    {
            string name;
    public:
    static  int money;
            Family(const string name="name", int money=0):name(name) {
                    this->money += money;
            }
    };   
    int Family::money = 0;

    int main(void)
    {
            printf("妈妈带来3000元\n");
            Family mather("mother", 3000);
            printf("爸爸带来6000元\n");
            Family father("father", 6000);

            cout << "妈妈查看账户" << mather.money << endl;
            cout << "爸爸查看账户" << father.money << endl;

            return 0;
    }

    例子分析:

    本例中,定义了一个家庭成员类Family,包含成员名和小金库(money),作为
    一个来说,小金库应该是家庭成员共享的,因此将其声明为了static。

    在Family类的构造函数中,实现了对money的累计。

    在main函数中,定义了两个对象,一个是father,另一个是mather,
    他们各自向小金库贡献了一笔钱,因为money是共享的,money实现了
    父亲和母亲金钱流动的通信。


(2)类的静态函数成员
    (1)定义方式
        只需在成员函数前面加static即可

(2)不依赖于对象而存在

(3)可以使用类名访问,访问形式与访问静态数据成员同
    使用类名访问静态成员函数时,对象空间可能还并不存在存在。

(4)可以使用对象名访问,这个时候对象必须存在

(5)普通的成员函数可以调用静态成员函数,这个时候对象必须存在
    普通成员函数依赖于对象而存在

(6)反过来,静态成员函数不能访问普通数据成员,不能调用普通成员函数,
    不能使用this指针,this代表当前独享,因为使用类名访问静态成员
    函数时,可能还没有任何实例化对象存在。

(7)静态成员函数只能访问静态数据成员
    同样的道理,因为调用静态函数,对象可能并不存在,因此普通成员
    肯定也不存在,这个时候存在的只有静态成员。

(8)实例化的所有对象共享相同的静态成员函数。

例子:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <iostream>
    #include <string>
    #include <cassert>

    using namespace std;
    class Family
    {
            string name;
    public:
    static  int money;

            Family(const string name="name", int money=0):name(name) {
                    this->money += money;
            }

            static int get_money() {
                    return money; //this.money,使用this访问是错误的
            }
    };

    int Family::money = 0;

    int main(void)
    {
            printf("妈妈带来3000元\n");
            Family mather("mother", 3000);
            printf("爸爸带来6000元\n");
            Family father("father", 6000);

            cout << "妈妈查看账户" << mather.get_money() << endl;
            cout << "爸爸查看账户" << father.get_money() << endl;

            printf("儿子拿走1000元\n");
            Family son("son", -1000);
            cout << "儿子查看账户" << son.get_money() << endl;

            cout << "银行查看账户" << Family::get_money() << endl;

            return 0;
    }

    例子分析:
    在Family类中,定义了一个静态成员函数get_money,在例子分别使用
    对象名和两名两种方式访问静态函数,需要注意的是在静态函数中不能
    使用this指针,也不能访问普通数据成员和普通成员函数。    
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值