探索C++的奥秘之类和对象(下)

1. 再谈构造函数

1.1 构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值

一个类要初始化成员变量有两种方式,第一种方式叫做函数体赋值,还有一种叫做初始化列表

class Date
{
public:
    Date(int year, int month, int day)
    {

        //下面这些是函数体内赋值,也就是
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year;
    int _month;
    int _day;
};

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。 

1.2 初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。初始化列表是成员定义的地方,构造函数是对每个成员进行初始化,不叫作定义。

注意:

1. 每个成员变量在初始化列表中最多只能出现一次,不出现也是可以的(初始化只能初始化一次)

2. 类中包含以下成员,必须放在初始化列表位置进行初始化:

引用成员变量

const成员变量

自定义类型成员(且该类没有默认构造函数时)

class A
{
public:
    //A(int a = 0)
    //    :_a(a)
    //{
    //    cout << "A(int a = 0)" << endl;
    //}
    A(int a)
        :_a(a)
    {
        cout << "A(int a = 0)" << endl;
    }
private:
    int _a;
};

class B
{
public:
    //初始化列表
    B(int a, int ref)
        :_n(a)
        ,_ref(ref)
        ,_x(2)    //如果这里显示定义了,就不会用缺省值
        ,_aobj(10)//对于自定义类型会去调用它的拷贝构造,如果我们没写,编译器会自动生成
    {
        //这个地方叫做函数体内赋值
    }
private:
    A _aobj; // 没有默认构造函数
    int& _ref; // 引用
    const int _n; // const 
    int _x = 1; //内置类型不做处理,这里的1是缺省值,缺省值是给初始化列表的
};

int main()
{
    //对象整体定义的地方
    B bb1(10,1);
    //B bb2(10, 1);
    return 0;
}

 函数体内赋值,不能对const成员进行初始化

初始化列表代替的是函数体内赋值,建议以后还是用初始化列表去初始化。

3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

class A
{
public:
    A(int a)
        :_a1(a)
        , _a2(_a1)
    {}

    void Print() {
        cout << _a1 << " " << _a2 << endl;
    }
private:
    int _a2;
    int _a1;
};

int main() {
    A aa(1);
    aa.Print();
}

A. 输出1 1

B.程序崩溃

C.编译不通过

D.输出1 随机值

正确答案是D

 什么时候程序才会崩溃?

野指针、内存越界,比较严重的问题程序才会崩溃。函数体类不受影响,就是按照语句正常走,也就是逐语句的执行。

typedef int DataType;
class Stack
{
public:
    Stack(size_t capacity = 10)
        :_top(0)
        , _capacity(capacity)
        , _array((int*)malloc(sizeof(int)* _capacity))
    {
        if (nullptr == _array)
        {

            perror("malloc申请空间失败");
            exit(-1);
        }
        memset(_array, 0, sizeof(int) * capacity);
    }

    void Push(const DataType& data)
    {
        _array[_top] = data;
        _top++;
    }

    ~Stack()
    {
        if (_array)
        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _top = 0;
        }
    }

private:
    DataType* _array;
    int _top;
    int _capacity;
};
int main()
{
    Stack s1;
    return 0;
}

编译上面的代码会出现一个断言错误,为什么会崩溃呢?因为这个里面已经涉及到了指针的问题,_capacity是个随机值,开出来的空间又会很大,声明的顺序和定义的顺序保持一致就不会出现刚才的问题。

1.3 explicit关键字

构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参 数的构造函数具体表现:

1. 构造函数只有一个参数

2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值

3. 全缺省构造函数

class A
{
public:
    A(int a)
        :_a(a)
    {}
    void Print()
    {
        cout << _a << endl;
    }
private:
    int _a;
};
int main()
{
    A a1(1);
    //下面这个地方是隐式类型转换,是整型转换成自定义类型

    //按语法的原意是用2去构造一个A的临时对象,临时对象再拷贝构造aa2,老的编译器一

    //般不会优化,新的编译器会优化为用2直接去构造,aa1和aa2的结果是一样的,但是过程

    //是不一样的
    A a2 = 2;//给一个默认值,又不想在初始化列表初始化
    a2.Print();
    return 0;
}

//explicit修饰构造函数,禁止类型转换

2. static成员

2.1 概念

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化

面试题:实现一个类,计算程序中创建出了多少个类对象。 

__LINE__这个宏是取自于哪一行

静态成员变量的生命周期是全局的,如果在类里面定义,就会报错,要在类外面定义。

而且必须这样写:int A::scout = 0;但是有人会有疑问了,这不是私有吗,但是我们可以理解我们可以突破一次私有。

如果是私有的,这里就不能访问,如果是公有的,就可以访问。_scout属于整个类,它也是属于每个对象共享,在这指定类域共有就可以了,私有就不行了。还有一种方法是定义一个成员函数就可以了,静态成员只要突破类域和访问限定符就可以访问,静态成员函数没有this指针,指定类域和访问限定符就可以访问。静态成员函数和静态成员变量不能调用非静态成员函数和非静态成员变量,非静态的成员函数调用需要this指针。静态成员函数既可以在类中定义,也可以在类外面定义。静态成员变量不能给缺省值,缺省值是给初始化列表的。

2.2 特性

1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区

2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问

4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员

5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

 静态成员变量和静态成员函数一般是配套出现的。

class A
    {
    public:
        A() 
        {
            ++_scount;
        }
    
        A(const A& t) 
        {
            ++_scount;
        }
    
        ~A() 
        { 
            --_scount;
        }
    
    
        //void Func1()
        //{
        //    // 非静态能否调用静态:可以
        //    GetACount();
        //}
    
        //void Func2()
        //{
        //    ++_a1;
        //}
    
        // 没有this指针,指定类域和访问限定符就可以访问
        static int GetACount();
        
    private:
        // 成员变量 -- 属于每个一个类对象,存储对象里面
        int _a1 = 1;
        int _a2 = 2;
    //public:
        // 静态成员变量 -- 属于类,属于类的每个对象共享,存储在静态区
        static int _scount;
    };
    
    // 全局位置,类外面定义
    int A::_scount = 0;
int     A::GetACount()
    {
        // 静态能否调用非静态:不可以。非静态的成员函数调用需要this指针,我没有this
        // Func2();

        //_a1++;
        return _scount;
    }
    A aa0;
    
    void Func()
    {
        static A aa2;
        cout << __LINE__ << ":" << aa2.GetACount() << endl;
    
        // 全局变量的劣势:任何地方都可以随意改变
        //_scount++;
    }
    
int main()
{
    cout <<__LINE__<<":"<< A::GetACount() << endl;  // 1
    A aa1;
        
    Func();  // 3
    Func();  // 3
    
    return 0;
}

补充: 

// 设计一个类,在类外面只能在栈上创建对象
// 设计一个类,在类外面只能在堆上创建对象
class A
{
public:
    static A GetStackObj()
    {
        A aa;
        return aa;
    }

    static A* GetHeapObj()
    {
        return new A;
    }
private:

    //在这个地方定义对象会自动调用构造
    A()
    {}

private:
    int _a1 = 1;
    int _a2 = 2;
};

int main()
{
    //static A aa1;   //  静态区
    //A aa2;          //  栈 
    //A* ptr = new A; //  堆

    //如果是下面这样的,调用这个成员函数需要对象,但是调用这个函数又是在获取对象。

    //GetStackObj();
    A::GetStackObj();
    A::GetHeapObj();

    return 0;
}

 3. 友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元分为:友元函数和友元类

3.1 友元函数

问题:现在尝试去重载operator,然后发现没办法将operator重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator重载成全局函数。但又会导致类外没办 法访问成员,此时就需要友元来解决。operator>>同理。

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声 明,声明时需要加friend关键字。

class Date
{
    friend ostream& operator<<(ostream& _cout, const Date& d);
    friend istream& operator>>(istream& _cin, Date& d);
public:
    Date(int year = 1900, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {}

private:
    int _year;
    int _month;
    int _day;
};

ostream& operator<<(ostream& _cout, const Date& d)
{
    _cout << d._year << "-" << d._month << "-" << d._day;
    return _cout;
}

istream& operator>>(istream& _cin, Date& d)
{
    _cin >> d._year;
    _cin >> d._month;
    _cin >> d._day;
    return _cin;
}

int main()
{
    Date d;
    cin >> d;
    cout << d << endl;
    return 0;
}

说明:

友元函数可访问类的私有和保护成员,但不是类的成员函数

友元函数不能用const修饰

友元函数可以在类定义的任何地方声明,不受类访问限定符限制

一个函数可以是多个类的友元函数

友元函数的调用与普通函数的调用原理相同

3.2 友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

友元关系是单向的,不具有交换性。

比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

友元关系不能传递

如果B是A的友元,C是B的友元,则不能说明C时A的友元。

友元关系不能继承,在继承位置再给大家详细介绍。

class Time
{
    friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
    Time(int hour = 0, int minute = 0, int second = 0)
        : _hour(hour)
        , _minute(minute)
        , _second(second)
    {}

private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {}

    void SetTimeOfDate(int hour, int minute, int second)
    {
        // 直接访问时间类私有的成员变量
        _t._hour = hour;
        _t._minute = minute;
        _t._second = second;
    }

private:
    int _year;
    int _month;
    int _day;
    Time _t;
};

 4. 内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中 的所有成员。但是外部类不是内部类的友元。

特性:

1. 内部类可以定义在外部类的public、protected、private都是可以的。

2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。

3. sizeof(外部类)=外部类,和内部类没有任何关系。

class A
{
private:
    static int k;
    int h;
public:
    class B // B天生就是A的友元
    {
    public:
        void foo(const A& a)
        {
            cout << k << endl;//OK
            cout << a.h << endl;//OK
        }
    };
};

int A::k = 1;

int main()
{
    cout << sizeof(A) << endl;//4
    return 0;
}

不用算k的大小,k没有存在对象里面,也不用算B,因为A类里面没有B的对象。

除非是下面这样定义

5.匿名对象

class A
{
public:
    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a)" << endl;
    }

    ~A()
    {
        cout << "~A()" << endl;
    }
private:
    int _a;
};

class Solution
{
public:
    int Sum_Solution(int n)
    {
        cout << "int Sum_Solution(int n)" << endl;
        return n;
    }
    ~Solution()
    {
        cout << "~Solution()" << endl;
    }
};

int main()
{
    A(1);   //匿名对象--生命周期在当前行
    A aa1;//有名对象--生命周期在当前函数局部域
    Solution s1;
    s1.Sum_Solution(10);
    Solution().Sum_Solution(20);
    return 0;
}

匿名对象有一个特点是:即用即销毁。并且如果构造函数有几个参数就要传几个参数,前提是构造函数不是全缺省。匿名对象和有名对象的区别是匿名对象没有名字。匿名对象和临时对象一致,具有常性。

const 引用能延长匿名对象的生命周期,使得它不会被立即析构,生命周期在当前函数局部域

直接优化为构造 + 拷贝构造。

void Func1(A aa)
{}

A Func5()
{
    A aa;
    return aa;
}

int main()
{
    A aa1;
    Func1(aa1); // 不会优化
    Func1(A(1)); // 构造+拷贝构造 ->优化为构造
    Func1(1);    // 构造+拷贝构造 ->优化为构造,和下面的原理一样
    A aa2 = 1;  // 构造+拷贝构造 ->优化为构造
    return 0;
}