类和对象(下)

初始化列表

☀️1.用途:

给对象的所有内部的成员变量初始化

☀️2.格式:

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

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

☀️3.特性:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。

  2. 类中包含以下成员时,必须放在初始化列表位置进行初始化:引用成员变量、const成员变量、自定义类型成员(且该类没有默认构造函数时)。这些成员无法在构造函数中被赋值。

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

例题:

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再_a1,因此在初始化列表中先执行_a2(_a1),将_a2赋值为随机值(因为此时的_a1是随机值),再执行_a1(a),将_a1赋值为1。最终打印的顺序是先_a1再_a2,因此输出“1 随机值”。

在这里插入图片描述

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

☀️4.举例:

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

☀️5.初始化列表和构造函数体赋值的区别

构造函数体赋值本质上不是初始化,初始化列表是初始化:
初始化只能进行一次,但在函数体内可以实现对一个变量多次赋值,因此构造函数体内赋值不是初始化;
初始化列表要求每个成员变量在列表中只能出现一次,就是在保证初始化只能进行一次,因此初始化列表是初始化。

隐式转换成类类型

对于内置类型,可以进行类型转换,如double d = 1语句就是将整型1隐式转换为double类型。同样的,也可以将内置类型隐式转换成自定义类型。

☀️1.隐式转换成为类对象的前提

对任何编译器而言,转换成的这个类对象的构造函数只有单个参数,或者除第一个参数无默认值其余均有默认值。

构造函数只有单个参数的情况:

Date(int year)
 :_year(year)
 {}

除第一个参数无默认值其余均有默认值的情况:

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

☀️2.隐式转换内部过程

内置类型转自定义类型是由构造函数支持的,构造函数中的参数是什么类型,就支持什么类型的数据转成类类型。

例1:A aa3 = 3;(整型->类类型)

构造函数以一个整型作为参数;
类和对象(下)_第1张图片
内部过程:
①用3构造一个临时对象
②用这个临时对象拷贝构造对象aa3

类和对象(下)_第2张图片

例2:const A& ra = 3;(整型->类引用)

在这里插入图片描述
(和例1一样,构造函数以一个整型作为参数)
内部过程:
①先用3构造一个临时对象
②ra就是这个临时对象的别名

注:引用起别名时,需要注意权限对等(可平移和缩小,不可放大)。隐式转换成引用类型的话需要在引用前加上const,因为本质上是将一个临时变量转换成引用,临时变量具有常属性,需要用const修饰的变量来接收临时变量(权限平移)

例3:浮点型->整型->类类型

在这里插入图片描述
(和例1例2一样,构造函数以一个整型作为参数)
内部过程:(隐式转换了两次)
①先将double类型的3.33隐式转换成int类型的3
②再用3构造一个临时对象
③用这个临时对象拷贝构造对象aa4

例4:指针->类类型

注意:和整型->类类型不同:整型->类类型需要的构造函数是以整型作为参数的;而指针->类类型需要以指针类型的变量作为该构造函数的参数
构造函数以一个整型指针作为参数;
类和对象(下)_第3张图片
类和对象(下)_第4张图片

explicit关键字

☀️1.用途:

给构造函数前加上explicit关键字后,无法进行隐式转换
类和对象(下)_第5张图片
类和对象(下)_第6张图片

☀️2.强制转换

如果在有explicit关键字的情况下,非要进行类型转换,只能强制转换:
在这里插入图片描述

static成员

☀️1.概念

声明为static的类成员(包含成员变量和成员函数)称为类的静态成员;
用static修饰的成员变量称为静态成员变量;
用static修饰的成员函数称为静态成员函数。

☀️ 2.特性

(1)静态成员(变量+函数)为所有类对象所共享,不属于某个具体的对象,存放在静态区。

(2)静态成员变量必须在类内声明,声明时要加static关键字,声明时不可以有初始值;类外定义,定义时不添加static关键字,但要指明类域,定义时再设置初始值。静态成员函数的声明和定义不用分开。

类和对象(下)_第7张图片

(3)静态成员(变量+函数)可用“类名::静态成员”或者“对象.静态成员”来访问或调用。但是“对象.静态成员”这个方法并不是让编译器从这个对象中来找到count变量,而是告诉编译器去对象所属的类域中去找count。

对类内静态成员变量的访问:
类和对象(下)_第8张图片
在这里插入图片描述
可见二者访问的是同一个东西,同一块地址。

对类内静态成员函数的调用(注意要加类域限定符):
在这里插入图片描述

(4)静态成员(变量+函数)也是类的成员,受public、protected、private 访问限定符的限制

(5)静态成员变量不会走初始化列表程序,该变量的本质就是全局变量。初始化列表时初始化某一个对象的,因此普通成员变量要走初始化列表程序,缺省值也是给初始化列表的。被static修饰的变量是共享的,因此不走初始化列表。

(6)静态成员函数的优势:

  1. 普通成员函数必须用对象调用,其实对象是为了提供this指针,而静态成员函数没有this指针,可以在没有对象的情况下尽情调用;
  2. 提高运行效率,告诉编译器你只需要去这个类域里面找这个函数就好,不用去别处。

(7)静态成员函数没有隐藏的this指针,内部不可以访问类里面的非静态成员变量

类和对象(下)_第9张图片

☀️3.实现一个类,计算程序中创建出了多少个类对象。

思路:

借助类对象在生成和销毁时自动调用构造函数(或拷贝构造函数)、析构函数的特性,可以定义一个全局变量用来计数,每调用一次构造函数或析构函数,就+1,每调用一次析构函数就-1。

(1)❌计数变量的命名不要用count:

因为库里面count是一个函数,命名冲突。
解决命名冲突需要注释掉“using namespace std”,但这样后序比较麻烦,如果想使用库里的东西的话需要加限定符“std::”。

(2)❌不能将count弄成类内部的成员变量:

这样每新生成一个对象,都会新生成一个count,count的初始值都是0,无法起到计数作用。
类和对象(下)_第10张图片

(3)❌可以将count放进一个新的命名空间,但有风险:

外部可以随意访问这个新命名空间内的变量,可能导致变量被恶意修改从而无法得到正确的值。
类和对象(下)_第11张图片

(4)✅最好地方式还是用静态变量(private)计数,用静态函数得到这个数(public):

class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
//声明
static int GetACount() { return _scount; }
private:
//声明
static int _scount;
};

//定义
int A::_scount = 0;

void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}

①由于变量是静态的,被所有对象共同拥有,从而可以累加计数;
②变量被private修饰,外界无法直接得到,从而保证结果不被恶意篡改;
③外部想要得到变量计算出来的值,需要一个public函数;
④用函数得到这个累加值,因此该函数也要是静态的才能知道类加值是多少;
⑤静态成员函数内无this指针,只能通过返回值的方式得到值。

只能通过这个静态函数得到值,不可以通过其修改,进一步保证了封装性和数据的准确。

☀️4.oj:求1+2+···+n

链接: https://www.nowcoder.com/share/jump/7711188001706953836169

题目描述:

要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句
类和对象(下)_第12张图片

思路:

定义一个类类型的数组,数组有多少个元素就要创建多少个类对象,借助每创建一个对象自动调用构造函数特性,实现求和。需要定义两个静态变量_i和_ret,_i用来自动加一,_ret用来将所有_i加和。

初始代码:

class Solution {
public:
    int Sum_Solution(int n) {
        
    }
};

提交代码:

#include 
class sum
{
public:
sum(){
    _ret+=_i;
    _i++;
}
static int GetRet(){
    return _ret;
}
private:
static int _i;
static int _ret;
};

int sum::_i=1;
int sum::_ret=0;

class Solution {
public:
    int Sum_Solution(int n) {
      sum arr[n];
     return sum::GetRet() ; 
    }
};

友元(友元函数+友元类)

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

☀️1.友元函数

(1)概念

在类的内部声明,声明时需要加friend关键字;在类外部定义。
友元函数可以直接访问类的私有成员。

(2)特性

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

以流输入操作符重载为例,学习友元函数:

重载流插入操作符函数operator<<,符号<<左边必须是cout,右边是自定义的对象,因此为了参数顺序正确,只能将函数定义在类的外部(定义在类内的话,默认第一个参数为this指针,使得参数顺序错误),然而这个重载的函数需要用到类对象的私有成员变量,因此需要友元这种突破封装的方式,使得类外函数可以自由使用类内成员变量。

class Date
{
//类内部声明
friend ostream& operator<<(ostream& _cout, const 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; 
}

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

成功调用重载的<<操作符函数:
在这里插入图片描述

☀️2.友元类

(1)概念

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

(2)特性

  1. 友元关系是单向的,不具有交换性。A是B的友元类,A可以自由访问B类的所有成员变量,但B不可以访问A。

  2. 友元关系不能传递。如果C是B的友元, B是A的友元,则不能说明C时A的友元。

内部类

☀️1.概念

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

☀️2.特性

(1)内部类天生就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

(2)内部类可以定义在外部类的public、protected、private,受访问限定符和权限的限制。private修饰的类始终无法被外部使用。

(3)内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。

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

证明:类B定义在类A内部,两个类各自有一个整型的成员变量,计算此时B类型的大小:

class A 
{
private:
	int a;
public:
	class B
	{
	public:
		int b;
	};

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

在这里插入图片描述
说明计算外部类A的时候不会计算上B。

☀️ 3.与“有一个类类型成员变量”概念的区分

类B定义在类A外部,在类A内部有一个B类型的成员变量,计算类A的大小:

class B
{
public:
	int b;
};
class A 
{
	int a;
	B b;
};
int main() {
	cout << sizeof(A) << endl;
	return 0;
}

在这里插入图片描述
说明计算类A的时候会计算上B,类B的大小是类A的大小的一部分。

☀️4.对oj“求1+2+···+n”的优化

在最初版本的基础上进行了如下几方面的优化:

  1. 将计数的静态变量i和ret定义到了外部类Sum_Solution中
  2. 将类sum定义到了类Sum_Solution的内部,sum作为内部类,从而sum的构造函数可以直接使用两个静态变量进行计数
  3. sum中不需要增加一个public函数来返回运算结果,ret反正在外部类内呢,直接返回ret就行
class Solution {
    class sum
    {
        public:
        sum(){
            ret+=i++;
        }
    };
public:
    int Sum_Solution(int n) {
        sum arr[n];
        return ret;
    }
private:
    static int i;
    static int ret;
};
int Solution::i=1;
int Solution::ret=0;

拷贝对象时的一些编译器优化

☀️1.补充知识:区分拷贝构造和赋值拷贝

(1)拷贝构造:已经存在了对象aa1,然后用aa1的模板创建新对象aa2和aa3。

(aa2和aa3分别代表拷贝构造的两种写法)

A aa1(1);
A aa2(aa1);
A aa3=aa1;

(2)赋值拷贝:存在两个对象aa1和aa2,将一个的值赋给另一个。

A aa1(1);
A aa2(2);
aa1=aa2;

☀️2.新编译器的优化原则:

在同一个表达式中,先后出现两次构造函数(或拷贝构造)时,编译器会将两函数合并成为一个函数,即两个步骤合并为一步。

具体哪两个函数合并为哪一个函数如下图:
类和对象(下)_第13张图片

☀️3.新编译器优化案例

现在有一个类A,其中有1个整型成员变量,public部分显式写出来4个函数,依次是构造、拷贝构造、=运算符重载函数、析构函数:
(增加了打印的语句,方便得知编译器内部到底用了哪些函数及函数调用的过程)

class A
{
public:
	A(int a) {
		_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) {
		_a = aa._a;
		cout << "A& operator=(const A& aa)" << endl;
		return *this;
	}
	~A() {
		cout << "~A()" << endl;
	}
private:
	int _a = 1;
};

基于类A展示以下优化案例

例1:

int main() {
	A aa1 = 1;
}
原本过程:
  1. 将整型1隐式转换为A类型,即用1创建一个临时对象
    (构造函数)
  2. 将临时对象拷贝赋值给对象aa1
    (拷贝构造函数)
  3. 临时对象被复制完毕,就销毁临时对象
    (析构函数)
  4. main函数结束,销毁aa1对象
    (析构函数)
编译器优化:

构造+拷贝构造->构造,最终会被优化为一个构造函数,即直接用1构造对象aa1,省去了构造临时对象和销毁临时对象的过程

最终运行结果:

在这里插入图片描述

例2:

int main() {
	const A& aa2 = 1;
}
内部过程:
  1. A& aa2代表aa2是一个对象的别名
  2. 这个对象肯定不能是整形数据2了,只能是由2隐式转换的对象
  3. 隐式转换即用2构造临时对象,aa2就是这个临时对象的别名
    (构造函数)
  4. 语句运行完后,就销毁临时对象
    (析构函数)
最终运行结果:

在这里插入图片描述

例3:

void func1(A aa) {

}
int main() {
	A aa3(1);
	func1(aa3);
}
内部过程:
  1. 用1构造一个对象aa3
    (构造函数)
  2. 传值调用func1,以aa3的拷贝作为参数,即用aa3拷贝构造一个临时对象
    (拷贝构造函数)
  3. 调用完func1函数,销毁临时对象
    (析构函数)
  4. main函数运行结束,销毁对象aa3
    (析构函数)
最终运行结果:

类和对象(下)_第14张图片

例4:

void func1(A aa) {

}
int main() {
	A aa3(1);
	
	func1(aa3);
	
	func1(A(1));
}
原本过程:
  1. 用1构造对象aa3
    (构造函数)
  2. 以aa3作为参数,传值调用func1,即生成用aa3拷贝构造而成的临时变量
    (拷贝构造函数)
  3. 调用完func1,销毁临时变量
    (析构函数)
  4. 又调用一次func1,这次参数为A(A(1))。先用1构造临时变量A(1),再用这个临时变量拷贝构造另一个临时变量,作为func1的参数
    (构造函数+拷贝构造函数)
  5. 运行完这句话,销毁临时变量A(1)和另一个临时变量
    (析构函数+析构函数)
  6. main函数结束,销毁aa3
    (析构函数)
编译器优化:

构造函数+拷贝构造函数->构造函数。将原本的第4步过程中先后出现的构造函数+拷贝构造函数,优化成1个构造函数;对应的,将原本的第5步过程中的两个析构函数优化的只剩1个析构函数(构造函数从2个变成1个了,则析构肯定要跟着变)。

最终运行结果:

类和对象(下)_第15张图片
类和对象(下)_第16张图片

例5:

A func2() {
	A aa(1);
	return aa;
}
int main() {
	A aa4(2);
	aa4 = func2();
	return 0;
}
内部过程:
  1. 用2构造对象aa4
    (构造函数)
  2. 需要给对象aa4重新赋值,赋func2函数返回的值,调用func2函数
  3. func2函数内,用1创建对象aa
    (构造函数)
  4. func2函数内,传值返回aa,传值返回的是由aa拷贝构造的临时对象
    (拷贝构造函数)
  5. func2函数调用结束,销毁aa对象
    (析构函数)
  6. 使用operator=函数,使得aa4接收到返回值,然后临时对象销毁
    (析构函数)
  7. main函数运行结束,销毁aa4对象
    (析构函数)
最终运行结果:

类和对象(下)_第17张图片

类和对象(下)_第18张图片

例6:

A func2() {
	A aa(1);
	return aa;
}
int main() {
	A aa5=func2();

    func2();

	return 0;
}
原本过程:

对于语句1:A aa5=func2();

  1. 需要用func2的返回值来拷贝构造aa5,因此需要先进入func2
  2. func2中,先用1构造aa
    (构造函数)
  3. func2中,传值返回aa,因此需要用aa拷贝构造一个临时对象
    (拷贝构造函数)
  4. 用临时对象拷贝构造main中的对象aa5
    (拷贝构造函数)
  5. func2函数结束,销毁aa
    (析构函数)
  6. 拷贝构造结束,销毁临时对象
    (析构函数)

对于语句2:func2();

  1. 进入func2,先构造aa
    (构造函数)
  2. 传值返回,用aa拷贝构造临时对象
    (拷贝构造函数)
  3. func调用结束,销毁aa
    (析构函数)
  4. main函数结束,销毁aa5和func2返回的临时对象
    (析构函数)
编译器优化:

拷贝构造+拷贝构造=拷贝构造。对于语句1:A aa5=func2();,会将3和4两个连续的拷贝构造函数合并成1个拷贝构造函数,相当于跳过了拷贝构造临时对象的过程,因此也不需要析构临时对象

最终运行结果:

类和对象(下)_第19张图片
类和对象(下)_第20张图片
类和对象(下)_第21张图片

☀️注:不同编译器或不同版本有优化差异

  1. 上述的案例都是vs2019的debug版本下的结果,如果换成release版本,会优化地更多。
    比如,对于案例5,优化成以下样子:
    类和对象(下)_第22张图片
  2. 不同编译器运行的结果可能会根据优化的程度有不同
  3. 优化等级越高,则拷贝构造次数越少,但可能因为激进的优化,导致原先能运行的程序可能运行不了

你可能感兴趣的:(c++,开发语言)