C++:类和对象(下)

发布于:2025-09-15 ⋅ 阅读:(18) ⋅ 点赞:(0)

目录

一、再探构造函数

1.1初始化列表的语法形式

1.2、初始化列表的注意事项

1.3、练习

二、类型转换exp

三、static 成员(静态成员变量)

3.1、相关细节

3.2、静态成员函数与普通成员函数的区别

3.3、静态成员变量和静态成员函数的使用场景

3.4、练习:求和

3.5、练习:判断调用顺序

四、友元

4.1、语法形式

4.2、实例介绍

4.3、友元函数和友元类的注意事项

五、内部类

5.1、内部类的概念

5.2、内部类的注意事项

六、匿名对象

七、对象拷贝时的编译器优化


一、再探构造函数

        C++给构造函数提供了两种初始化方式:函数体内赋值 和 初始化列表。这里主要学习初始化列表。

1.1初始化列表的语法形式

        初始化列表以 开始, 以 分隔,每个成员后面的小括号里放初始化表达式。例如:

class Date
{
private:
    int _year;
    int _month;
    int _day;
public:
    Date(int year = 1999, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {
        //一般用来第二次操作
    }
};

这就是初始化列表,以前在函数体内操作的顶多叫赋值,而这个是初始化。那么,有了初始化列表,构造函数的函数体里面写什么?

class Stak
{
private:
    int* _arr;
    int _top;
    int _capacity;
public:
    Stack(int size = 2)
        : _arr((int*)malloc(sizeof(int)*size)
        , _top(0)
        , _capacity(size)
    {
        for(int i = 0 ; i < size ;i++)
        {
            arr[i] = 0;
        }
    }
};

这里只是举个简单的例子,告诉大家,有了初始化列表,构造函数体不是就荒废了,也是有用的。

1.2、初始化列表的注意事项

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

(2)在语法上可以理解初始化列表为每个成员变量定义初始化的地方;

(3)有些类型的成员变量必须在初始化列表进行初始化:引用类型的成员变量,const修饰的成员变量,没有默认构造函数的类类型变量。这些类型的变量有个共性:在定义时必须给参数初始化。当然,除非这些类型在声明的时候给了缺省值。

class B
{
    int _bb;
    B(int bb){
        _bb = bb;
    }
};


class A
{
private:
    B _bb;
    int& _a2;
    const int _a3;
public:
    A(B bb , int& a2 ,int a3)
        :_bb(bb)   //没有默认构造函数的类类型必须在初始化列表初始化
        ,_a2(a2)   //引用类型的变量必须在初始化列表初始化
        ,_a3(a3)   //必须在初始化列表初始化
    {
    }
};

(4)C++11 支持在成员变量声明的位置给缺省值,主要是给没有显示在初始化列表的初始化成员使用的;

class A
{
private:
    int_ a1 = 1; //这就是在声明的位置给缺省值
    //…………
};

(5)那些你不在初始化列表初始化的成员,也会走一遍初始化列表,如果在声明位置有缺省值,如果没有缺省值,内置类型取决于编译器,自定义类类型会调用它的默认构造函数,如果没有默认构造函数,就会报错。

        什么意思?就是按照初始化的优先级: 初始化列表 > 声明时的缺省值。

(6)初始化列表中按照成员变量在类中的声明顺序进行初始化,与成员在初始化列表中出现的先后顺序无关。

总结:

1、建议尽量多使用初始化列表

2、如果没有在初始化列表初始化,尽量给缺省值。

1.3、练习

判断 a1 和 a2 的值

class A
{
public:
    A(int a)
        :_a1(a)
        ,_a2(_a1)
private:
    int _a2 = 2;
    int _a1 = 2;
};

int main()
{
    A aa(1);
    return 0;
}

根绝C++规则,初始化列表的顺序只与声明的顺序有关,所以,先初始化 _a2  = _a1 ,而 _a1 此时还未初始化,为随机值,所以_a2为随机值,然后再初始化_a1 = 1。所以答案是:_a1 = 1 ,_a2 = 随机值。

二、类型转换

int a = 1;
double b = a;
const double& ref = a;

a 的类型为 int ,b 的类型为 double ,当初始化 b 的时候,编译器会产生一个临时变量,将 a 拷贝进去,然后在用这个临时变量去初始化 b 。这就是隐式类型转换。这种临时变量具有常性,所以如果涉及引用的类型转换,就需要用 const 修饰,防止权限扩大,例如上述代码中的 ref。

(1)C++ 也支持内置类型隐式类型转换为类类型,需要有相关的内置类型为参数的构造函数;

class A
{
private:
    int _a1 = 1;
    int _a2 = 2;
public:
    A(int a)
        :_a1(a)
    {}
};

int main()
{
    A a1(1);  //构造函数
    A a2 = 1; //隐式类型转换,但编译器可能会优化为构造函数
    
    const A& ref2 = a1;  //正常的引用
    const A& ref3 = 1;   //引用的是中间产生的临时变量
    return 0;
}

(2)explicit 在狗仔函数前修饰,表示不再支持隐式类型转换

(3)类类型的对象之间也可以隐式转换,需要相应的构造函数支持

三、static 成员(静态成员变量)

         被 static 修饰的成员变量成为静态成员变量

3.1、相关细节

(1)静态成员变量为所有类的对象所共享,不属于某个具体的对象,不存在对象中,而是存放在静态区;

(2)由于类的声明不分配空间(因为没有实例化),所以静态成员变量一定要在类外面进行初始化

(3)用 static 修饰的成员函数称为静态成员函数,静态成员数没有 this 指针;

(4)静态成员也受private,public ,protected 的限制

(5)静态成员变量不在声明位置给缺省值,也不走构造函数初始化列表;

(6)突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。

3.2、静态成员函数与普通成员函数的区别

(1)静态成员函数不带 this 指针,普通成员函数自带 this 指针。如果静态成员函数想使用 this 指针,需要建立一个函数来传递;

(2)静态成员函数只能直接访问静态成员,而普通成员函数可以直接访问静态和非静态的成员;

(3)静态成员函数可以通过对象实例化调用,也可以通过雷明调用,而普通成员函数必须通过对象实例化调用;

(4)静态成员函数不能使用 const 和 virtual,普通成员函数可以;

3.3、静态成员变量和静态成员函数的使用场景

静态成员变量:当所有类的对象共享一个数据时;

静态成员函数:工具函数或辅助函数。

3.4、练习:求和

https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

这道题按理来说非常简单,但是它什么都不让用,循环不让用,条件不让用,就连乘除也不让用,怎么办呢?

创建一个类,_sum 用来存放总和 ,_a 来表示每项的大小,都用静态成员表示。然后在题目里创建一个大小为 n 的该类类型的数组,那么就会调用 n 个构造函数。我们在构造函数里动手脚就可以了。注意,静态成员必须在类外面初始化,静态成员函数只能直接访问静态成员变量!

class Sum
{
private:
    static int _a ;
    static int _ret;
public:
    Sum()
    {
        _ret += _a;
        ++_a;
    }
    static int GetSum()
    {
        return _ret;
    }
};

int Sum::_ret = 0;
int Sum::_a = 1;

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

3.5、练习:判断调用顺序

在下列代码中,请给出 A B C D的构造函数调用顺序和析构函数的调用顺序:

class A;
class B;
class C;

C c;
int main()
{
    A a;
    B b;
    static D d;
    return 0;
}

构造函数调用顺序:C A B D

析构函数调用顺序:B A D C

c 是全局变量,d 是静态变量,二者都存放在静态区

四、友元

在C++中,友元是一种允许非成员函数或其他类访问类的私有和保护成员的机制。

4.1、语法形式

//友元声明
friend void func(参数);

在你想要的友元的函数前加一个 friend 即可。

4.2、实例介绍

class A;

class B
{
private:
	int _b1 = 1;
	int _b2 = 2;
public:
	void func(const A& aa);
};

class A
{
    //声明:B里面的func是A的友元函数
	friend void B::func(const A& aa);
private:
	int _a1 = 1;
	int _a2 = 2;
};

void B::func(const A& aa)
{
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
}

int main()
{
	A aa;
	B bb;
	bb.func(aa);
	return 0;
}

上述代码就上将 B 的成员函数作为了 A 的友元函数,可以访问A的成员。同样的,如果 B 的大部分函数都要与 A 有练习,可以将 B 作为 A 的友元类。

4.3、友元函数和友元类的注意事项

(1)友元函数仅仅是一种声明,不是类的成员函数;

(2)友元函数不受访问限定符的限制,放在类的哪里都是一样的;

(3)友元类关系不能传递,例如:A 是 B 的友元,B 是 C 的友元,不能说 A 是 C 的友元

(4)友元关系是单向的, 例如:A 是 B 的友元,但 B 不是 A 的友元

(6)友元增加了耦合度,但是破坏了封装,不宜多用。

五、内部类

5.1、内部类的概念

        内部类就是在一个类的内部定义另一个类

例如:

class A
{
	friend void B::func(const A& aa);
private:
	int _a1 = 1;
	int _a2 = 2;
public:
	class C
	{
	private:
		int _c1 = 1;
		int _c2 = 2;
	public:
		void Print()
		{
			cout << _c2 << endl;
		}
	};
};

上述代码中,类 A 里面又定义了类 C ,C就是内部类

5.2、内部类的注意事项

(1)内部类是一个独立的类,只是受到了外部类的访问限定符的限制;

(2)内部类默认是外部类的友元;

六、匿名对象

        匿名对象(也称为临时对象)是C++中没有名称的临时对象,它们在创建后立即使用,并在表达式结束时被销毁。例如:

class A;
A aa;  //有名对象
A();   //匿名对象

有名对象的生命周期就是它的作用域,匿名对象的生命周期只在这一行,下一行就销毁了。

那么,匿名对象能干什么呢?

(1)匿名对象可以作为函数参数

(2)调用类的成员函数

(3)简化代码

七、对象拷贝时的编译器优化

        现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。