我可以不吃啊 发表于 2025-3-16 11:11:29

C++底子 [三] - 面向对象三

初始化列表

起首,初始化列表是我们的祖师爷本贾尼博士为了解决在某些成员变量在界说时必须初始化的环境。这个初始化列表实在发生在构造函数之前,也就是实例化整个对象时先对所有的成员都进行了初始化
初始化的概念区分 

在之前的博客学习中,我们已经学习了【C++】的六大默认成员函数 ,想必大家已经对构造函数已经比较熟悉了,可是大家是否遇到过,在构造函数后面跟了一个冒号,这个题目让我很是狐疑
https://i-blog.csdnimg.cn/direct/7d88e7e478374b36b15f5b82592da967.png
在了解 初始化列表之前,我们起首回首两个重要的知识:
   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;
};         上面这个Date类是我们之前写过的,这里有一个它的有参构造函数,固然在这个构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化。构造函数体中的语句只能将其称为【赋初值】,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
class Date
{
public:
        Date(int year = 2022, int month = 5, int day = 24)
        {
                _year = year;
                _year = 2023; //第二次赋值
      _year = 2024; //第三次赋值
                _month = month;
                _day = day;
        }
private:
        int _year;
        int _month;
        int _day;
}; 既然构造函数体的语句只能称作为赋初值,现在,可否有一种方式进行初始化呢?即初始化列表初始化。
总结    


[*]我们之前写的构造函数实在并不是对成员变量进行初始化而是进行赋初值。
[*]如果想要对成员变量进行初始化,必要用到初始化列表
初始化列表的概念理解

 以一个冒号 “ :” 开始,接着是一个以 , 分隔的数据成员列表,每个"成员变量"后面跟一个放在 ()的初始值或表达式
比方如下代码:
class Date
{
public:
    //构造函数: -->初始化列表初始化
        Date(int year = 2024, int month = 8, int day = 2)
                :_year(year)
                , _month(month)
                , _day(day)
        {}
private:
        int _year;
        int _month;
        int _day;
}; 固然,我可以在初始化列表初始化,也可以在大括号内进行赋值:
Date(int year = 2024, int month = 8, int day = 2)
        :_year(year)
        , _month(month)
{
        _day = day;
} 初始化列表的注意事项

   初始化列表可以以为就是对象成员变量界说的地方
https://i-blog.csdnimg.cn/direct/227747ee6a2c40bba4a1eb8bbc163b05.png
   每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
https://i-blog.csdnimg.cn/direct/79d010cc1eea462180ce22a51a17822d.png
   类中包含以下成员,必须放在初始化列表位置进行初始化

[*]引用成员变量
[*]const成员变量
[*]自界说类型成员(该类没有默认构造函数)
先前我们都知道引用的变量和const变量只能在界说时初始化,而普通的变量在界说时不强求初始化,所以我们就不能按照如下的方式操纵:
https://i-blog.csdnimg.cn/direct/1bcb77221ec24f83a2fa959fe30bae92.png
 成员变量为const和引用的时候-----正确的代码为:
class Date
{
public:
    //析构函数
        Date(int year = 12, int month = 10, int day = 1)
                :_year(year), _month(month), _day(day)
        {}

        void Printf()
        {
                cout << "year为:" << _year << endl;
                cout << "month为:" << _year << endl;
                cout << "day为:" << _year << endl;
        }
private:
        //定义时不强求初始化,后面可以再赋值修改
        int _year;   //声明

        //const修饰的变量 和 引用的变量 需要在定义的时候就进行初始化
        const int _month;
        int& _day;
};

int main()
{
        Date d1;
        d1.Printf();
        return 0;
} 自界说类型成员(该类没有默认构造函数)同样也得在初始化列表进行初始化: 
class A
{
public:
        A(int x)//不是默认构造函数,因为接受一个参数
                :_x(x)
        {}

private:
        int _x;
};

class Date
{
public:
        Date(int a) //在初始化列表对自定义类型 _aa 进行初始化
                :_aa(a)
        {}
private:
        A _aa;
}; 注意这里的条件,肯定要是没有默认构造函数的自界说类型成员才得在初始化列表进行初始化,而默认构造函数简单来说就是不必要传参的函数
   成员变量在类中声明序次就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后序次无关
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
 解析:注意成员变量在类中声明序次就是其在初始化列表中的初始化顺序,既然_a2先声明,则必然进入初始化列表要先实行, _a2(_a1) 。意思是说拿_a1去初始化_a2,不外此时的_a1照旧随机值,天然_a2即为随机值,随后实行:_a1(a)。拿a初始化_a1,所以输出的值为1和随机值。
explicit关键字

在我们本身平常写 C++ 代码的时候,较少会用到 explicit关键字 。但是在C++相关的尺度类库中,看到explicit关键字的频率照旧很高的。既然出现的频率这么高,那么我们就来看看explicit关键字的作用到底是干什么的。
什么是explicit关键字

  explicit是C++中的一个关键字,它用来修饰只有一个参数的类构造函数,以表明该构造函数是显式的,而非隐式的。当使用explicit修饰构造函数时,它将克制类对象之间的隐式转换,以及克制隐式调用拷贝构造函数。 
既然表明中提到了 类的构造函数  那么下面我将从构造函数中详细的给大家,解说explicit其中的含义。
   构造函数还具有类型转换的作用
  在理解 explicit 关键字 之前,我们必须要了解构造函数的类型转换作用,以便于我们更好的理解 explicit 关键字
单参构造函数与explicit关键字

照旧来说说老朋侪日期类,我们通过下面这个日期类进行解说
class Date
{
public:
// 构造函数
        Date(int year)
                :_year(year)    // 初始化列表
        {}

private:
        int _year;
        int _month = 3;
        int _day = 31;
};  对于下面的 d1 很清楚肯定是调用了有参构造进行初始化,不外对于 d2 来说,也是一种构造方式
int main()
{
    // d1 和 d2 都会调用构造函数
        Date d1(2022);   

        Date d2 = 2023;
       
        return 0;
} 我们依旧通过调试来看就会非常清楚,这种 【Date d2 = 2023】 写法也会去调用构造函数
https://i-blog.csdnimg.cn/direct/a7105a6727304871b147a8c4cdb5495b.gif
此时,大家可能会产生疑问,这种构造方式从来没有见过,为什么 Date d2 = 2023 会调用 构造函数呢?  实在这都是因为有【隐式类型转换】的存在,下面我将从一个简单的例子来为大家解说。
像下面将一个int类型的数值赋值给到一个double类型的数据,此时就会产生一个隐式类型转换
int i = 1;
double d = i;  对于类型转换而言,这里并不是将值直接赋值给到左边的对象,而是在中间呢会产生一个临时变量,比方右边的这个 i 会先去构造一个临时变量,这个临时变量的类型是  。把它里面的值初始化为 1,然后再通过这个临时对象进行拷贝构造给d,这就是编译器会做的一件事
https://i-blog.csdnimg.cn/direct/eefb997b60f6486f8440896cee1aac7e.png
那对于这个 d2 实在也是一样,2023会先去构造一个临时对象,这个临时对象的类型是把它里面的year初始化为2023,然后再通过这个临时对象进行拷贝构造给到d2
https://i-blog.csdnimg.cn/direct/cf3bedb3b97b4654b7a756e879710cc9.png
不是说构造函数有初始化列表吗?拷贝构造怎么去初始化呢?
 别忘了【拷贝构造】也是属于构造函数的一种哦,也是会有初始化列表的
//拷贝构造
Date(const Date& d)
        :_year(d._year)
        ,_month(d._month)
        ,_day(d._day)
{} 刚才说到了中间会产生一个临时对象,而且会调用构造 + 拷贝构造,那此时我们在Date类中写一个拷贝构造函数,调试再去看看会不会去进行调用


[*]很显着没有,我在进入Date类后不绝在按F11,但是却进不到拷贝构造中,这是为什么呢?
https://i-blog.csdnimg.cn/direct/26d4d99022db4359a243f069926bed6c.gif
缘故原由实在在于编译器在这里地方做了一个优化,将【构造 + 拷贝构造】优化成了【一个构造】,因为编译器在这里以为构造再加拷贝构造太费事了,干脆就合二为一了。实在对于这里的优化不同编译器是有区别的,像一下VC++、DevC++可能就不会去优化,越是新的编译器越可能去进行这种优化。
但是怎么知道中间赋值这一块产生了临时对象呢?如果不清楚编译器的优化机制这一块肯定就会以为这里只有一个构造 
这点确实是,若是我现在不是直接赋值了,而是去做一个引用,此时会发生什么呢?
Date& d3 = 2024;
 可以看到,报出了一个错误,缘故原由就在于d3是一个Date类型,2024则是一个内置类型的数据
https://i-blog.csdnimg.cn/direct/6adf4b6feab14d7ab0f690260b31229d.png
一个常量让d3共用会造成权限放大!!


[*]但若是我在前面加一个const做修饰后,就不会出现题目了,这是为什么呢?
[*]实在这里的真正缘故原由就在于产生的这个【临时变量】(临时变量具有常性),它就是通过Date类的构造函数构造出来的,同类型之间可以做引用。尚有一点就是临时变量具有常性,所以给到一个const类型修饰对象不会有题目 
从这里我们就可以看到在中间赋值的时候是产生了临时变量。
但若是你不想让这种隐式类型转换发生怎么办呢?此时就可以使用到C++中的一个关键字叫做explicit 


[*]它加在构造函数的前面进行修饰,有了它就不会发生上面的这一系列事儿了,它会【克制类型转换】
explicit Date(int year)
        :_year(year)
{} https://i-blog.csdnimg.cn/direct/542e0076a61643afa955a2375235a8dd.png
多参构造函数与explicit关键字

//多参构造函数
Date(int year, int month ,int day = 31)
        :_year(year)
        ,_month(month)
        ,_day(day)
{} https://i-blog.csdnimg.cn/direct/e97d60600016455dbc531450a95fbac6.png
根据从右往左缺省的规则,我们在初始化构造的时候要给到2个参数,d1没有题目传入了两个参数,但是若是像上面那样沿袭单参构造函数这么去初始化还行得通吗?很显着不行,编译器报出了错误
这个时候就要使用到我们C++11中的新特性了,在对多参构造进行初始化的时候在外面加上一个{}就可以了,可能你以为这种写法像是C语言里面结构体的初始化,但现实不是,而是在调用多参构造函数
Date d2 = { 2023, 3 };
https://i-blog.csdnimg.cn/direct/306122b67ead47a09b1f9c02168e0e64.gif


[*]不仅如此,对于下面这种也同样适用,调用构造去产生一个临时对象
const Date& d3 = { 2024, 4 };
那要怎样去防止这样的隐式类型转换的发生呢,照旧可以使用到explicit关键字吗?
//多参构造函数
explicit Date(int year, int month ,int day = 31)
        :_year(year)
        ,_month(month)
        ,_day(day)
{} 尚有一种破例,当缺省参数从右往左给到两个的时候,此时只必要传入一个实参即可,那也就相称于是单参构造explicit关键字依旧可以起到作用
explicit Date(int year, int month = 3,int day = 31)
        :_year(year)
        ,_month(month)
        ,_day(day)
{} 友元

友元提供了一种突破封装的方式,偶尔提供了便利。但是友元会增长耦合度,粉碎了封装,所以
友元不宜多用。
友元分为:友元函数和友元类
友元函数

去重载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;
} 注意:
1、友元函数可访问类的私有和保护成员,但不是类的成员函数
2、友元函数不能用const修饰(没有this指针)
3、友元函数可以在类界说的任何地方声明,不受类访问限定符限制
4、一个函数可以是多个类的友元函数
5、友元函数的调用与普通函数的调用原理相同
友元类 

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
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;
}; 注意:
1、友元关系是单向的,不具有交换性。
        比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接
访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
2、友元关系不能转达
       如果C是B的友元, B是A的友元,则不能说明C时A的友元。 
3、友元关系不能继承 
内部类

内部类的原理:B的房子的图纸在A的图纸里面。比如说我是地产大亨,你之前帮助过我。迩来你跟我说你要在你工作的地方建个别墅。我说好,你建吧,并且我还附赠你一个别墅,把你老家也建造起来。此时 你工作的别墅就不包含你老家的别墅。然后你说,工作的别墅我不要了,能不能单独给我老家建起来,大亨说:你还想空手套白狼是吧 想得美!
内部类的界说

内部类是在一个类的成员部门界说的另一个类。
内部类是⼀个独立的类。跟界说在 全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类界说的对象中不包含内部类。
   盘算外部类对象的大小就不会将内部类的成员包括在内
https://i-blog.csdnimg.cn/direct/83e3247f8203462c8bdcb95ee0538280.png
内部类与外部类 

   内部类访问外部类的成员
简单来说:内部类默认是外部类的友元类


[*]内部类可以界说在外部类的public、protected、private中都是可以的。
[*]内部类可以直接访问外部类中的static、罗列成员、不必要外部类的对象名。
[*]内部类访问外部类的普通成员,必要借助外部类对象(否则无法得知访问的是哪一个对象的)https://i-blog.csdnimg.cn/direct/1a3808e5002241b7b3003207afd4eda0.png
这里cout<<h<<endl;是一个非常常见的错误。因为内部类是一个独立的类,不属于外部类,所以此时还没有外部类的对象,显然也不存在h。
而k就不同了,静态成员不必要外部类的对象就已存在,所以这里k是OK的。
想要在内部类访问外部类的普通成员,就必要通过外部类对象的方式,比如下方代码,转达一个外部类对象作为参数就可以访问外部类成员
https://i-blog.csdnimg.cn/direct/adf0864d137e48ebaa17a86159a867e0.png
内部类的经典实例
使用的特点就是内部类天生是外部类的友元
class Solution {
public:
    int Sum_Solution(int n)
    {
      Sum a;//根据创建数组多少次就调用多少次构造函数
      return _sum;
    }
private:
    class Sum
    {
      public:
         Sum()
         {
                _sum+=_i;
                ++_i;
         }
    };//内部类可以访问外部类的静态成员
    static int _i;
    static int _sum;
};
int Solution::_i=1;
int Solution::_sum=0;   外部类访问内部类的成员 
内部类不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
内部类在界说它的外围类的作用域内是可见的,但在外围类外部是不可见的。 


[*]如果内部类受外部类公有限定符限制,必要通过外部类限定符限定的方式创建内部类对象
[*]如果内部类是受外部私有或保护限定符限制,那么在类外无法创建内部类的对象(如果你不想外部可以创建内部类的对象,就可以这么做)
[*]外部类无法访问内部类的私有成员
class A
{
private:
    static int _k;
    int _h = 1;
public:
    class B // B默认就是A的友元

    {
    public:
      void foo(const A& a)
      {
            cout << _k << endl; //OK
            cout << a._h << endl; //OK
      }
    private:
      static int a;
    };
    void fun()
    {
      //cout << B::a << endl;
      //外部类无法访问内部类的私有成员
    }
   
};
int A::_k = 1;
int main()
{
   
    A::B b;//假如内部类受外部类公有限定符限制,可以这样创建内部类对象
    A aa;
   
    return 0;
}  内部类的使用场景



[*]封装和潜伏实现细节:内部类可以潜伏实现细节,使得外围类的接口更加简洁。—内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了
[*]实现辅助类:内部类可以作为外围类的辅助类,用于实现一些具体的功能,而不影响外围类的整体结构。
[*]避免命名辩说:通过内部类,可以避免不同命名空间或类中的命名辩说。
[*]访问权限控制:内部类可以更好地控制对特定成员或方法的访问权限。
 匿名对象

匿名对象的生命周期在当前行
匿名对象具有常性
const引用会延长匿名对象的的生命周期,声明周期跟当前函数的作用域
   即用即销毁




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: C++底子 [三] - 面向对象三