【C++】类和对象(三)

其他类和对象文章:
类和对象(一)

类和对象(二)

再谈构造函数

函数体中的赋值

创建一个对象时,会调用构造函数,对成员变量进行定义与初始化

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

调用如上代码后,对象就会被初始化为一个值。那么构造函数的函数体中的语句就是初始化吗?并不是,在函数体中我们可以对一个变量多次赋值,但是初始化只能进行一次。也就是说函数体中的语句并不是初始化,而是赋值

那么对象究竟是在哪里初始化的呢?其实在构造函数中,还有一个初始化列表

初始化列表

构造函数的初始化列表格式:在参数列表外面,以冒号开始,接着以逗号分隔数据成员列表,每个成员变量后面都有一个括号,括号内是变量的初始值或者一个表达式

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

我们可以测试一下

Date d1(2024, 1, 1);

通过调试进入 d1 对象的构造函数,可以看到,在调用构造函数时,就会走初始化列表

在这里插入图片描述

初始化列表有如下特性:

  1. 初始化列表中的每个成员变量只能出现一次,因为初始化只能进行一次

在这里插入图片描述

  1. 成员变量的初始化顺序是按照声明顺序来的,与初始化列表中的顺序无关
class A
{
public:
        A(int x = 1)
                :_a1(x)
                ,_a2(_a1)
        {}

private:
        int _a2;
        int _a1;
};

如上代码,虽然初始化列表中成员变量的顺序是 _a1 _a2,但是初始化顺序是按照声明顺序来的,先初始化 _a2,再初始化 _a1。我们用 _a1 的值初始化 _a2,此时 _a1 还没有初始化,是随机值,所以 _a2 就被初始化成随机值

在这里插入图片描述

三种成员必须使用初始化列表

那么初始化列表就只有这些作用了吗?当然不是,有三种成员必须使用初始化列表:

  1. 引用成员变量
  2. const 成员变量
  3. 没有默认构造函数的自定义成员

对于引用和const变量来说,只有一次初始化的机会:

  • 引用变量初始化后,后面只可以赋值,而不可以改变指向
  • const变量初始化后,就不可以再修改了

所以这两种变量如果想初始化只可以走初始化列表,不可以在函数体中赋值

我们在之前的学习中已经知道,如果类 A 中有一个成员是另一个类 B,那么创建 A 类对象调用 A 构造函数时,就会自动调用 B 的构造函数

class B
{
public:
        B()
        {
                cout << "B()" << endl;
        }
private:
        int _b;
};

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

private:
        int _a;
        B _b;
};

在这里插入图片描述

以上是 B 类拥有默认构造函数情况下可以正常运行,如果没有默认构造,就会发生报错

class B
{
public:
        B(int x) // 非默认构造
        {
                cout << "B()" << endl;
        }
private:
        int _b;
};

在这里插入图片描述

这时候就要通过初始化列表,显式调用 B 类的合适的构造函数来初始化,如下:

class A
{
public:
        A(int x = 1)
                :_a(x)
                ,_b(10) // 显式调用B类对象的合适的构造函数
        {
                cout << "A()" << endl;
        }

private:
        int _a;
        B _b;
};

单参数构造函数的类型转换

构造函数不仅可以用来构造和初始化对象,对于单个参数或者除了第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。现有如下代码:

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

private:
        int _a;
};

通常我们实例化一个对象是这样做的:

A a(10);

而现在,我们可以这样做:直接赋值给自定义类型

A a1 = 10;

在这里插入图片描述

一个是整型,一个是自定义类型,将整型赋值给自定义类型,这为什么可行呢?

以上现象就和将一个 double 类型变量赋值给一个 int 类型变量一样,中间发生了隐式类型转换

在这里插入图片描述

将一个 int 类型赋值给一个自定义类型 A,这中间发生了单参数构造函数的类型转换:先构造一个临时无名对象,然后无名对象赋值给目标

在这里插入图片描述

中间会产生一个临时对象,我们可以这样验证

A& a1 = 10;

在这里插入图片描述

因为临时变量具有常性,不可更改,直接使用引用接收会造成权限的放大,所以应该使用常引用来接收

const A& a1 = 10;

当构造函数中有多个参数时,我们依然可以直接赋值,只不过要将参数放在花括号中

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

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

在这里插入图片描述

单参数构造函数的类型转换还是很好用的,例如 Stack 类中的数据类型是自定义类型,我们入一个数据可能要先构造出相应的对象,再将对象入栈

Stack s1;
A d1(10);
s.push(d1);

有了类型转换,就可以直接一步到位,还是很方便的

s.push(10);

如果不想使用自动转换,可以在相应构造函数的前面加上关键字explicit

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

在这里插入图片描述

static成员

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

类的静态成员具有以下特性:

  1. 静态成员变量在类中只是声明,需要在类外定义,定义时不需要加static,需要指定类域
class A
{
public:
        A(int x = 1)
                :_a(x)
        {
                cout << "A()" << endl;
        }


private:
        int _a;
        static int _sa; // 声明
};

// 类外定义
int A::_sa = 10;
  1. 访问静态成员有两种方式:
  • 通过对象访问,对象.静态成员
  • 直接通过来访问,类名::静态成员
class A
{
public:
        A(int x = 1)
                :_a(x)
        {
                cout << "A()" << endl;
        }

        // 静态成员函数
        static void PrintStatic()
        {
                cout << _sa << endl;
        }


private:
        int _a;
        static int _sa; // 声明
};

A a;
a.PrintStatic(); // 对象.静态成员
A::PrintStatic(); // 类名::静态成员
  1. 静态成员不是属于某一个类对象的,是属于整个类的,每个类对象都可以访问。存放于静态区
void ModifyStatic()
{
        _sa++;
}

在这里插入图片描述

  1. 静态成员函数不属于任何一个对象,所以没有隐含的 this 指针,也不能访问任何非静态成员变量
  2. 静态成员也是类的成员,所以受 public、private、protected 访问限定符的限制

友元

定义在类外的函数受到访问限定符的限制,不可以访问类中的私有成员,但是通过声明类外函数为类的友元函数,就可以突破限制,之前我们在【类和对象(二)】重载流提取流插入运算符已经使用过

友元提供了一种突破封装的方法,虽然使用较为方便,但是破环了封装,增加了耦合度,所以能不用就不用

友元分为友元函数友元类

友元函数

友元函数定义在类外,是普通函数,只需在类中的任意位置声明一下即可,声明时需要加关键字friend

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

        // 声明友元
        friend void PrintA(const A a);
private:
        int _a;
};
// 友元函数
void PrintA(const A a)
{
        cout << a._a << endl;
}

在这里插入图片描述

友元函数有以下特性:

  1. 友元函数可以访问类的私有和保护成员
  2. 友元函数只是普通函数,并不是类的成员函数。声明在类中,定义在类外
  3. 因为并不是成员函数,没有 this 指针,所以友元函数不可以被const修饰
  4. 一个函数可以是多个类的友元
  5. 友元函数本质上就是一个普通函数,调用原理与普通函数无异

友元类

不仅函数可以成为类的友元,类也可以成为类的友元。如果类 A 是 类 B 的友元类,那么 A 就可以访问 B 的私有与保护成员。

友元类的用法与友元函数相同,在类中的任意位置声明友元类即可

class B
{
public:
        B(int x = 1)
                :_b(x)
        {
                cout << "b()" << endl;
        }
        // 声明友元类 A是B的友元
        friend class A;
private:
        int _b;
};

class A
{
public:
        A(int x = 1)
                :_a(x)
        {
                cout << "A()" << endl;
        }
        // 修改 B 的私有成员
        void SetB(int x)
        {
                _ab._b = x;
        }
private:
        int _a;
        B _ab;
};

在这里插入图片描述

友元类特性如下:

  1. 友元关系是单向的,A 是 B 的友元,那么 A 就可以访问 B 的内部,但是反过来不可以,除非 A 和 B 互为友元
  2. 友元关系并不可以传递,A 是 B 的友元,B 是 C 的友元,并不能说明 A 是 C 的友元
  3. 友元关系不可被继承,后面的继承部分会说

内部类

一个类定义在另一个类中,这个类就是内部类

class A
{
public:
        A(int x = 1)
                :_a(x)
        {
                cout << "A()" << endl;
        }
        // B 就是一个内部类
        class B
        {
        public:
                B(int x = 1)
                        :_b(x)
                {
                        cout << "B()" << endl;
                }
        private:
                int _b;
        };

private:
        int _a;
};

内部类特性:

  • 虽然 B 是定义在 A 中的一个内部类,但是内部类是一个独立的类,不属于外部类。外部类并没有对内部类的任何特权,无法访问内部类的成员
  • 虽然内部类定义在外部类中,但是外部类的大小和内部类没有任何关系

在这里插入图片描述

  • 内部类只可以在外部类中使用,如果想要在外部类之外创建一个内部类对象,需要使用作用域解析运算符::,有点像命名空间
A::B b;
  • 内部类天生是外部类的友元,内部类可以通过外部类对象参数来访问外部类的成员
class B
{
public:
        B(int x = 1)
                :_b(x)
        {
                cout << "B()" << endl;
        }
        
        // 访问并修改外部类的成员
        void SetA(A& a, int x)
        {
                a._a = 10;
        }
private:
        int _b;
};

在这里插入图片描述

  • 内部类还可以直接访问外部类的静态成员,无需加类名或者对象名
class A
{
public:
        A(int x = 1)
                :_a(x)
        {
                cout << "A()" << endl;
        }
        // B 就是一个内部类
        class B
        {
        public:
                B(int x = 1)
                        :_b(x)
                {
                        cout << "B()" << endl;
                }

                void PrintSa()
                {
                        cout << _sa << endl;
                }
        private:
                int _b;
        };

private:
        int _a;
        // 外部类静态成员
        static int _sa;
};
int A::_sa = 111;

在这里插入图片描述

匿名对象

我们创建一个对象时,不给名字直接创建,就会创建出来一个匿名对象

// 匿名对象
A(10);
// 有名对象
A a1(10);

匿名对象的生命周期只有一行

在这里插入图片描述

可以看到,当匿名对象所在行执行完毕后,匿名对象就会调用析构函数

匿名对象有什么用呢?我们只想要调用类的成员函数,但又不想把成员函数写为静态的。这时用创建匿名对象调用函数,调用完毕后匿名对象销毁,还是很方便的

编译器对于构造的一些优化

在某些场景下,编译器会对连续的构造进行优化,以减少开销,提升效率。不同的编译器优化策略不同

构造+拷贝构造

A a = 1;

正常情况下,应该先用 1 构造一个临时对象,再用临时对象来拷贝构造a。但是编译器发现没必要,直接优化为一个构造,用 1 来构造 a

在这里插入图片描述

连续的拷贝构造

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

int main()
{
    f1();
}

正常情况下,应该构造aa,然后拷贝构造aa,形成一个临时变量,返回

在这里插入图片描述

但是编译器发现 f1 的结果没人接收,拷贝了也没什么意义,索性直接不拷贝了

在这里插入图片描述

那如果有人接收呢?

A f1()
{
    A aa;
    return aa;
}
A ret = f1();

那就应该是构造+拷贝构造+拷贝构造

在这里插入图片描述

而结果是这样的:

在这里插入图片描述

直接优化为一个构造,vs2022有点激进啊(流汗

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿洵Rain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值