【C++】类和对象(下)—— 再谈构造函数 | static成员 | C++11初始化补丁 | 友元

简介: 再谈构造函数 | static成员 | C++11初始化补丁 | 友元

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;
};

int main()
{
    Date d1(2022, 2, 22);
    return 0;
}

但是对于像const成员变量,必须在定义的时候同时初始化。如果在函数体内初始化就会报错 ——

<img src=" title="">

为此我们引入了初始化列表,就是给成员变量找到一个依次定义处理的地方。

1.2 初始化列表

语法格式:冒号开始,逗号间隔

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

private:
    int _year; //声明
    int _month;
    int _day;
};

int main()
{
    Date d1(2022, 2, 22); //实例化/定义一个对象
    return 0;
}

注:每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次),可以不出现(不是每个都必须在这儿初始化)

:heart: 1. 类中包含以下成员,必须放在初始化列表进行初始化。注意,初始化列表就是成员变量定义的地方!!这是理解这里的关键。

  • const成员变量

    因为const成员变量必须在定义的时候同时初始化

  • 引用成员

    因为引用成员变量必须在定义的时候同时初始化

    代码示例 ——

    class Date
    {
    public:
        Date(int year, int month, int day,int i)
            : _N(10)
            , _ref(i)
        {
            _year = year;
            _month = month;
            _day = day;
        }
    
    private:
        int _year; //声明
        int _month;
        int _day;
    
        const int _N; //const
        int& _ref;      //引用
    };
    
    int main()
    {
        int i = 0;
        Date d1(2022, 2, 22, i); //实例化/定义一个对象
        return 0;
    }
  • 没有默认构造函数自定义类型成员

    回忆:默认的构造函数,即不用传参的有三个 ——

    1. 我们不写编译器自己生成的
    2. 无参的
    3. 全缺省的

    没有默认构造函数(什么情况下没有?我们自己写了一个构造函数,还是带参的,编译器不再自动生成),编译器调不动,需要在定义的时候自己显式的传参去调。示例如下 ——

    class A
    {
    public:
        A(int a)
        {
            _a = a;
        }
    private:
        int _a;
    };
    
    class Date
    {
    public:
        Date(int year, int month, int day,int i)
            : _N(10)
            , _ref(i)
            , _aa(1)
        {
            _year = year;
            _month = month;
            _day = day;
        }
    
    private:
        int _year; //声明
        int _month;
        int _day;
    
        const int _N;
        int& _ref;
        A _aa; //没有默认构造函数的自定义类型成员
    };
    
    int main()
    {
        int i = 0;
        Date d1(2022, 2, 22, i); //实例化/定义一个对象
        return 0;
    }

其它成员变量,如int _year等在哪里初始化都可以。

:heart: 2. 建议尽量使用初始化列表,对于自定义成员变量,初始化列表可以提高效率。

对比 —— 为了观察调用情况,在成员函数内部打印

  • [ ] 不使用初始化列表

<img src=" title="">

  • [ ] 使用初始化列表

<img src=" title="">

总结:内置类型成员,在函数体和在初始化列表初始化都可以;自定义类型的成员,建议在初始化列表初始化,这样更高效。

代码如下 ——

#include<iostream>
using namespace std;

class A
{
public:
    // 构造函数 - 全缺省
    A(int a = 0)
    {
        cout << "A(int a = 0)" << endl;
    }
    // 拷贝构造
    A(const A& aa)
    {
        cout << "A(const A& aa)" << endl;
        _a = aa._a;
    }
    // 赋值重载
    A& operator=(const A& aa)
    {
        cout << "A& operator=(const A& a)" << endl;
        _a = aa._a;
        return *this;
    }
private:
    int _a;
};

class Date
{
public:
    //// 不使用初始化列表
    //Date(int year, int month, int day, const A& aa)
    //{
    //    _aa = aa;
    //    _year = year;
    //    _month = month;
    //    _day = day;
    //}

    //使用初始化列表
    Date(int year, int month, int day, const A& aa)
        : _aa(aa)
    {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year; //声明
    int _month;
    int _day;

    A _aa;
};

int main()
{
    A aa(10);
    Date d1(2022, 2, 22, aa); //实例化/定义一个对象
    
    //// 可以使用匿名对象,一行解决
    //Date d1(2022, 2, 22, A(10));
    return 0;
}

:heart: 3. 初始化列表中的初始化顺序是在类中的声明次序,与其在初始化列表中的先后次序无关、

问:
A.输出 1 1 
B.程序崩溃
C.编译不通过
D.输出1 随机值

class A {
public:
    A(int a)
        :_a1(a)        //2.
        , _a2(_a1)     //1.
    {}

    void Print() {
        cout << _a1 << " " << _a2 << endl;
    }
private:
    int _a2; //初始化顺序 - 声明顺序
    int _a1;
}

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

选dog。运行结果 ——

<img src=" title="">

建议一个类的声明顺序和初始化列表的出现顺序保持一致,这样就不容易出问题。

1.3 explicit关键字

阅读如下代码,为什么一个整型能转换为日期类?为了后续分析,我们还是在成员函数中打印。

#include<iostream>
using namespace std;

class Date
{
public:
    Date(int year)
        :_year(year)
    {
        cout << "Date(int year)" << endl;
    }

    Date(const Date& dd)
    {
        _year = dd._year;
        cout << "Date(const Date& year)" << endl;
    }
private:
    int _year;

};

int main()
{
    Date d1(2002);
    Date d2 = 2022;//思考?
    return 0;
}

为什么一个整型能转换为日期类?这其实是因为单参数的构造函数中发生了隐式类型转换

回忆C语言中隐式类型转换

    // 隐式类型转换 - 相近类型 -- 表示意义相似的类型
    double d = 1.1;
    int i = d;
    const int& i = d; //后文马上解释

    // 强制类型转换 - 无关类型
    int* p = &i;
    int j = (int)p;

回忆在C++入门讲常引用时,讲到隐式类型转换时会产生临时变量i其实是临时变量的引用,这里也类似。(临时变量具有常属性,不可修改,因此要加上const)

<img src=" title="">

这儿本来是用2022构造一个临时对象Date(2022),再用这个对象拷贝构造d2。但是C++在连续的过程中,编译器会优化多个构造,合二为一,因此这里被优化为直接就是一个构造<img src=" title="">

相当于这两句代码 ——

Date tmp(2022); //先构造
Date d2(tmp);    //再拷贝构造

由于这个单参数的构造函数,整形就可以构造一个日期类的对象。

上述代码可读性不是很好,用explicit修饰构造函数,将会禁止单参构造函数的隐式转换 ——

<img src=" title="">

2. static成员

2.1 静态成员变量

static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数

【面试题】实现一个类,计算中程序中创建出了多少个类对象。这并不好数,因为编译器可能有优化。

下面给出整个认知过程 ——

由于要对同一个变量进行++,比较朴素的想法是定义一个全局count,但是这是有问题的,如果如果有两个类共用一个count会有累加效应,且别人可以在外部随意更改。

#include<iostream>
using std::cout;
using std::endl;

int count = 0;

class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        count++;
    }
    A(const A& aa)
    {
        count++;
    }
private:
    int _a;
};

void f(A a)
{

}

int main()
{
    A a1;
    A a2 = 1;
    f(a1);
    cout << count << endl;
    count++; //存在问题:别人可以随意更改
    cout << count << endl;
    return 0;
}

如何把count与这个类深度绑定呢?改成成员变量吗?不可以,这样每一个对象都各自有一个count,不是对同一个变量++。这时我们要引入静态成员变量。

:heart: 1. 静态成员属于整个类,为所有类对象所共享,不属于某个具体的实例,生命周期为整个工程

:heart: 2. 静态成员变量必须在类外的全局定义,定义时不添加static关键字。注意:这是一个特例,只有这里能在类外访问私有。不然你想想,怎么对这个静态成员变量定义?

作为私有成员,怎么样在类外访问呢?只能提供一个公用的成员函数getCount。代码如下 ——

我们可以通过对象来访问成员函数getCount

#include<iostream>
using namespace std;

class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        _scount++;
    }
    
    A(const A& aa)
        : _a(aa._a)
    {
        _scount++;
    }
    
    int getCount()
    {
        return _scount;
    }
private:
    int _a;
    static int _scount; //静态成员变量
};

int A::_scount = 0;    //静态成员变量必须在类外的全局定义

void f(A a)
{

}

int main()
{
    A a1;
    A a2 = 1;
    f(a1);
    cout << a1.getCount() << endl; //通过对象来访问成员函数
    return 0;
}

那么有没有更好的方式,不需要定义对象就可以获取到呢?这要引入我们的静态成员函数。

2.2 静态成员函数

:heart: 静态成员函数没有隐藏的this指针只能访问静态成员变量和函数。不能访问任何非静态成员,这很合乎情理,你都没this指针

#include<iostream>
using namespace std;

class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        _scount++;
    }
    A(const A& aa)
        : _a(aa._a)
    {
        _scount++;
    }
    // 静态成员函数
    static int getCount()
    {
        return _scount;
    }
private:
    int _a;
    static int _scount;
};

int A::_scount = 0;

void f(A a)
{

}

int main()
{
    A a1;
    A a2 = 1;
    f(a1);
    cout << A::getCount() << endl; //可以通过类域来找
    cout << a1.getCount() << endl; //当然了,通过对象来找
    return 0;
}

静态成员函数,可以通过类域访问。当然通过对象来找也可以,但这并不意味着在对象里面找,它也不存在于对象里。

注:含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量。

3. C++11 的成员初始化新玩法

这其实是C++11打的一个补丁。众所周知,默认生成的构造函数,对内置类型不处理就是一个随机值,对自定义类型会调用它的构造函数处理。这有些不太合理。

于是C++11打了一个补丁,非静态成员变量,可以在函数声明给缺省值。若传参,就用传的参数对成员变量进行初始化;若没有传参,则用给定缺省值进行初始化。

#include<iostream>
using namespace std;

class B 
{
public:
    B(int b = 0)
        :_b(b)
    {}
    int _b;
};

class A 
{
public:
    void Print()
    {
        cout << a << endl;
    }
private:
    // 非静态成员变量,可以在成员声明时给缺省值。
    int a = 10;
};


int main()
{
    A a;
    a.Print();

    //A().Print(); //匿名对象调用也可

    return 0;
}

:heart: 注意:这不是初始化,因为这是声明,不能初始化,我对你哪个对象初始化?

且缺省值是比较宽泛的 ——

#include<iostream>
using namespace std;

class B 
{
public:
    B(int b = 0)
        :_b(b)
    {}
    int _b;
};

class A 
{
public:
    void Print()
    {
        cout << a << endl;
        cout << b._b << endl;
        cout << p << endl;
    }
private:
    // 非静态成员变量,可以在成员声明时给缺省值。
    int a = 10;
    B b = 20; // 因为单参数的构造函数,等价于B b = B(20);
    int *p = (int*)malloc(4);
    int arr[10] = { 1, 2, 3, 4, 5 }; //vs2019支持,2013不支持
    static int n;
};

int A::n = 10;

int main()
{
    A a;
    a.Print();

    //A().Print(); // 匿名对象调用也可

    return 0;
}

4. 友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元就像黄牛:cow:一样,破坏管理规则,会增加耦合度,破坏了封装,所以友元不宜多用

查阅 cplusplus.com - The C++ Resources Network 可知,coutcin对于内置类型之所以能“自动识别类型”,是因为库里面已经把函数重载都写好了。

<img src=" title="">

<img src=" title="">

那对于Date类这样的自定义类型,怎么样内置类型一样,直接使用流提取、流插入打印呢?

    Date d1(2022, 2, 23); //诶!刚好是19岁的最后一天
    d1.PrintWeekday();
    cout << d1; //like this?
    cin >> d2;    //like this?

现在我们尝试去重载operator<<,失败了,根本调不动这个函数。

<img src=" title="">

这是因为在运算符重载中,如果是双操作数的运算符重载,第一个参数也就是左操作数,第二个参数是右操作数。cout这个输出流对象和隐含的this指针在抢占第一个参数的位置。如果像下面这样,是能调起来,就是流倒灌了,您总不能这么用吧:sweat:

<img src=" title="">

于是把它挪到类外,将operator<<重载成全局函数,摆脱隐藏的this指针约束,但是又有类外无法访问成员的问题

注:这里为了支持连续输出,重载函数需要有返回值ostream&,原理类似于连续赋值,只不过cout的结合性是从左至右

<img src=" title="">

那么这里就需要引入友元来解决。

4.1 友元函数

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

注:

  • 友元函数想访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰(const修饰的是非静态成员函数,修饰的是tihs所指向的对象)
  • 友元函数只是一种声明,可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数只是在语法上突破了封装,调用与普通函数的调用和原理相同
#include<iostream>
using namespace std;

class Date
{
    friend ostream& operator << (ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);
public:
    Date(int year,int month,int day)
        : _year(year)
        , _month(month)
        , _day(day)
    {}

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

ostream& operator << (ostream& out,const Date& d)
{
    out << d._year << "/" << d._month << "/" << d._day << endl;
    return out;
}

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


int main()
{
    Date d1(2022, 2, 23); //19岁的最后一天,要快乐
    Date d2(2022, 2, 24); // 等解封了,我再出去过生日yeah!
    cout << d1 << d2;
    cin >> d1 >> d2;
    return 0;
}

注意:流提取操作符,需要写入,右操作数不能用const修饰

4.2 友元类

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

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

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

  • 友元关系不能传递

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

就像是,我“宣布”你是我好朋友,那我的东西你随便拿去用好了!你不是我好朋友,那可不行,就是这样~

#include<iostream>
using namespace std;

class Date; // 前置声明
class Time
{
    // 友元:声明Date为Time类的友元类,则在Date类中就直接访问Time类中的私有成员变量
    friend class Date; 
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;
};

5. 内部类

C++不喜欢用,java喜欢用。

如果一个类定义在另一个类的内部,这个类就叫做内部类。内部类天生就是就是外部类的友元类

  • [ ] 内部类和在全局定义的类是基本一样的只是内部类受到外部类A类域限制
  • [ ] 注意友元类的是单方面的,在如下代码中,内部类B天生就是A的友元,即B可以访问A的私有和保护,A不能访问B的私有和保护。就像是我是外部类,我把你捧在手心上,什么都给你,但是我爱你又与你无关。
  • [ ] 内部类可以定义在外部类的public、protected、private都是可以的
class A {
private:
    static int k;
    int h = 0;

public:
    class B
    {
    public:
        void foo(const A& a)
        {
            cout << k << endl;    //可以访问外部类的static
            cout << a.h << endl;//可以访问外部类的private
        }
    private:
        int _b;
    };
};

int A::k = 1;

int main()
{
    A::B b; //只是受类域的限制
    b.foo(A());
    return 0;
}

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

    cout << sizeof(A) << endl;

注:static成员变量放在静态区,不在对象中

<img src=" title="">

相关文章
|
1月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
23 3
|
1月前
|
存储 编译器 C++
C++入门3——类与对象2-1(类的6个默认成员函数)
C++入门3——类与对象2-1(类的6个默认成员函数)
30 1
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解2
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解
29 3
|
1月前
|
C++
C++构造函数初始化类对象
C++构造函数初始化类对象
18 0
|
8天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
35 4
|
9天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)