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

我们最后来到C++类和对象知识的收官之篇

目录

一、再来谈谈构造函数

1.1 使用构造函数体赋值

1.2 初始化列表

1.3 explicit关键字

二、static成员

三、匿名对象

四、友元

4.1 友元函数

4.2 友元类

五、内部类

六、关于一些编译器的优化


一、再来谈谈构造函数

1.1 使用构造函数体赋值

📋在我们定义类的构造函数时可以这样子:

class date
{
public:
    date(int year = 0, int month = 0, int day = 0)//构造函数
    {
        _year = year;
        _month = month;
        _day = day;
    }

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

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

📋另外我们如果类中成员类型出现了const所修饰的成员,在构造函数体中我们无法对其进行初始化:

这是因为const是常量,我们只能在定义时将其赋值(初始化),进入构造函数时const常量已经被定义过了,我们无法在构造函数中再对其进行修改

但是我们可以在定义成员的地方给每个成员缺省值呀,比如:

class date
{
public:
    date(int year = 0, int month = 0, int day = 0, int x = 0)
    {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year;
    int _month;
    int _day;
    const int _x = 10;//给_x一个缺省值
};

这个方法是可以,但是在C++11之前是不支持在定义处给缺省值的,所以为了方便定义类成员在构造函数中存在一个构造列表的概念:

1.2 初始化列表

📌初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

📋例如:

class date
{
public:
    date(int year = 0, int month = 0, int day = 0, int x = 0)
        //初始化列表    
        :
        _year(year),
        _month(month),
        _day(day),
        _x(x)
    {}

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

❗但是在我们使用初始化列表时要注意:

1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 引用成员变量、const成员变量、自定义类型成员(且该类没有默认构造函数时)必须放在初始化列表位置进行初始化

📋例如:

class A
{
public:
    A(int a)
        :_a(a)
    {}
private:
    int _a;
};
class B
{
public:
    B(int a, int ref)
        :_aobj(a)
        , _ref(ref)
        , _n(10)
    {}
private:
    A _aobj;  // 没有默认构造函数
    int& _ref;  // 引用
    const int _n; // const
};
3. 成员变量的初始化顺序是在类中声明次序,与其在初始化列表中的先后次序无关

📋例如:

class date
{
public:
    date(int n= 0)
        //初始化列表    
        :
        _month(n),
        _year(_month)
    {}
    void Print()
    {
        cout << _year << " " << _month << endl;
    }
private:
    int _year;
    int _month;
};
int main()
{
    date d1;
    d1.Print();
    return 0;
}

运行效果:

如上,_year成员先声明就先被初始化,而此时_month成员还未被初始化,最终造成了_year随机值的情况

1.3 explicit关键字

📋我们拿下面这个类来举例:

class date
{
public:
    date(int n= 0)
        :
        _month(n),
        _year(2023)
    {}
    void Print()
    {
        cout << _year << " " << _month << endl;
    }
private:
    int _year;
    int _month;
};

我们可以这样实例化对象(调用构造函数):

    date d1(0);

但是我们还有一种方法:

    date d1 = 8;//隐形类型转换

这种方法被称为隐形类型转换

int类型的常量8怎么就转换为date类型了呢?

💡这是因为8这个常量在编译时开辟了一个空间,这个空间自动将常量8转换为date类型的数据,再由构造拷贝函数将这个临时空间的值拷贝到d1中

但是上述过程是一些老编译器的做法,在新编译器中会直接将8作为形参传入到构造函数中,如此一来就完成了隐形类型转换

上面的构造函数只有一个参数(C++98),那多参数的构造函数怎么使用隐形类型转换呢?

使用{}将想要传入的值括起来即可(C++11):

class date
{
public:
    date(int n = 0, int x = 8)
        :
        _month(n),
        _year(x)
    {}
    void Print()
    {
        cout << _year << " " << _month << endl;
    }
private:
    int _year;
    int _month;
};
int main()
{
    date d1 = { 9,2008 };//多参数隐形类型转换
    d1.Print();
    return 0;
}

如果我们不想让系统进行隐形类型转换可以用到explicit这个关键字:

class date
{
public:
    explicit date(int n = 0)//使用explicit关键字
        :
        _month(n),
        _year(2023)
    {}
    void Print()
    {
        cout << _year << " " << _month << endl;
    }
private:
    int _year;
    int _month;
};

这样当我们直接使用=传入不同类型的数据时,编译器就不会进行隐形类型转换了:

二、static成员

对于static这个关键字想必大家都不陌生,但是在类中的static成员有着不一样的作用,我们现在来详细说一说:

📋现在有一道面试题:让你统计一下一个类一共创建了多少次对象

对于这问题我们可以使用一个全局变量,每创建一个对象就在构造函数或者构造拷贝函数中对其++,但是这种方法未免太搓了,现在我们使用static成员来看看:

class A
{
public:
    A()
    {
        ++_a;
    }
    void Print()
    {
        cout << _a << endl;
    }
private:
    static int _a;//声明
};
int A::_a = 0;//定义

效果如下:

在上面我们创建了一个类数组a[10](即该数组有10个元素,每个元素是A类的一个对象)和一个a2对象,总计一共创建了11个对象,在每次创建对象时对其内部static成员变量_a进行++,以此来统计出一共创建了多少对象

我们可以看到我们在类中声明的静态成员变量是属于整个类的,无论我们使用哪个对象其内部static成员都是同一个_a

❗但是要注意的是:在类中的静态变量不能在声明处赋值,一定要在类外进行初始化!

对于类中的函数,我们也可以使用static来修饰:

class A
{
public:
    A()
    {
        ++_a;
    }
    static void  Print()//使用static修饰的函数
    {
        //cout << _x << endl;//使用static的函数没有this指针,无法对其内部非静态成员变量进行访问
        cout << _a << endl;
    }
private:
    int _x;
    static int _a;
};

❗注意:静态成员函数没有隐藏的this指针,不能访问任何非静态成员

对于类中的静态成员函数我们可以使用类名和作用域限定符(::)来进行访问使用创建的对象名加.也可):

三、匿名对象

📋我们在这里顺便插一个小知识点,我们想调用类中的函数时,如果其函数不是static类型所修饰的,我们需要创建一个对象,再用这个对象调用所需要的函数:

class A
{
public:
    A()
    {
        ++_a;
    }
    void  Print()
    {
        cout << _a << endl;
    }
private:
    int _x;
    static int _a;
};
int A::_a = 0;
int main()
{
    A a;//创建一个a对象
    a.Print();//使用a这个对象调用Print函数
    return 0;
}

这样子十分的麻烦,不利于我们操作,现在我们来创建一个没有名字的匿名对象

📋对于类实例化所创建的对象我们可以不对其进行命名:

class A
{
public:
    A()
    {
        ++_a;
    }
    void  Print()
    {
        cout << _a << endl;
    }
    ~A()
    {
        cout << "end" << endl;
    }
private:
    int _x;
    static int _a;
};
int main()
{
    A().Print();//使用匿名对象来直接调用函数
    return 0;
}

这样子我们就不需要再创建一个有名字的对象再来调用来了

不过匿名对象的生命周期在于它所在的这一行,出了该行会被自动销毁

上面调试时,出了匿名对象该行,系统自动调用析构函数进行了销毁

但是凡事都有个例外,当我们使用const &来接受匿名对象时,匿名对象的生命周期就会延长至接受对象的作用域了:

class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
};
int main()
{
	const A& t = A();
	cout << "pause" << endl;
	return 0;
}

我们可以看到匿名对象A()并不是马上析构,而是当接受对象t生命周期结束后再进行析构

四、友元

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

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

4.1 友元函数

对于友元函数我们在类和对象(中)提到过,这里再介绍一下:

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

📌具体使用方法为:friend 已定义的函数 (在类中声明)

📋例如:

class date
{
    friend void operator<<(ostream& out, const date& d);//使用友元函数
public:
    date(int year = 0, int month = 0, int day = 0)
    {
        _year = year;
        _month = month;
        _day = day;
    }
 
private:
    int _year;
    int _month;
    int _day;
};
void operator<<(ostream& out, const date& d)
{
    out << d._year << "/" << d._month << "/" << d._day << endl;
}

❗注意:

友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同

4.2 友元类

既然被friend关键字修饰的函数可以访问一个类的私有成员,那一个类被friend关键字修饰了是不是可以访问另一个类的所有成员呢?

当然可以!

class A
{
    friend class B;//B是A的友元类(即B类可以访问A类的所有成员)
public:
    A()
        :_x(0)
    {
        ++_a;
    }
    void  Print()
    {
        cout << _a << endl;
    }
    ~A()
    {
        cout << "end" << endl;
    }
private:
    int _x;
    static int _a;
};
int A::_a = 0;
class B
{
public:
    void Print()
    {
        cout << _A._a << endl;//可以访问_A对象的私有成员
    }
private:
    A _A;
};

❗要注意:

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
友元关系是单向的,不具有交换性(比如上述A类和B类,在A类中声明B类为其友元类,那么可以在B类中直接访问A类的私有成员变量,但想在A类中访问B类中私有的成员变量则不行)
友元关系不能传递(如果C是B的友元, B是A的友元,则不能说明C时A的友元)
友元关系不能继承,在谈到继承时再给大家详细介绍

五、内部类

📌内部类的定义:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。

内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

📋例如:

class A
{
public:
    void APrint()
    {
        cout << _a <<endl;
        //A和B是独立的,A只能访问B的公有成员
        B b;
        b.BPrint(*this);
        //cout<<b._b   //访问不了B的私有成员
    }
private:
    //内部类
    class B//B是A的内部类
    {
    public:
        int _x = 0;
        void BPrint(const A& a)
        {
            cout << a._a << endl;//B是A类的友元可以访问其内部所有成员
        }
    private:
        int _b = 0;
    };
    //A的内部成员
    int _a = 0;
};

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

我们下面来看一下A类实例化的对象的大小:

这样我们很好的发现A实例化的对象是不包含B的,这很好的验证了内部类的独立性

❗下面要注意一点:内部类可以定义在外部类的public、protected、private都是可以的,但是受外部类作用限定符的约束

六、关于一些编译器的优化

在上面构造函数进行隐形类型传参时,我们可以发现编译器对其进行了优化

📋这种情况在VS2022中还很常见,下面是一些举例:

class A
{
public:
    A(int a = 0)
    :_a(a)
    {
        cout << "A(int a)" << endl;
    }
    A(const A& aa)
    :_a(aa._a)
    {
        cout << "A(const A& aa)" << endl;
    }
    A& operator=(const A& aa)
    {
        cout << "A& operator=(const A& aa)" << endl;
        if (this != &aa)
        {
            _a = aa._a;
        }
        return *this;
    }
    ~A()
    {
        cout << "~A()" << endl;
    }
private:
    int _a;
};
void f1(A aa)
{}
A f2()
{
    A aa;
    return aa;
}
int main()
{
    // 传值传参
    A aa1;
    f1(aa1);
    cout << endl;
    // 传值返回
    f2();
    cout << endl;
    // 隐式类型,连续构造+拷贝构造->优化为直接构造
    f1(1);
    // 一个表达式中,连续构造+拷贝构造->优化为一个构造
    f1(A(2));
    cout << endl;
    // 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
    A aa2 = f2();
    cout << endl;
    // 一个表达式中,连续拷贝构造+赋值重载->无法优化
    aa1 = f2();
    cout << endl;
    return 0;
}

根据以上规律我们可以总结出:

1、接收返回值对象,尽量拷贝构造方式接收,不要赋值接收
2、函数中返回对象时,尽量返回匿名对象
3、尽量使用const &传参


本期博客到这里就结束了,内容较多还请各位细细观看

欢迎在评论区指出不足呀~

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

1e-12

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

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

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

打赏作者

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

抵扣说明:

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

余额充值