当前位置: 首页> 娱乐> 明星 > 【C++】7.类和对象(5)

【C++】7.类和对象(5)

时间:2025/7/11 9:59:58来源:https://blog.csdn.net/hlyd520/article/details/142307717 浏览次数:0次

文章目录

  • 1. 再探构造函数
    • 1.1 构造函数体赋值
    • 1.2 初始化列表
    • 1.3 explicit关键字
  • 2 类型转换
  • 3.static成员
  • 4. 友元


1. 再探构造函数

  • 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

  • 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。

  • 引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。

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

  • 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。

  • 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。


1.1 构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

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

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


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

注意:

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  • 引用成员变量
  • const成员变量
  • 没有默认构造函数的类类型成员变量
class A
{
public:A(int a):_a(a){}
private:int _a;
};class B
{
public:B(int a, int ref)//这边也可以写成B(int a = 10, int ref),这样和:_aobj(10)是一样的:_aobj(a),_ref(ref),_n(10){}
private:A _aobj; // 没有默认构造函数int& _ref; // 引用const int _n; // const 
};
  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
class Time
{
public:Time(int hour = 0):_hour(hour){cout << "Time()" << endl;}
private:int _hour;
};class Date
{
public:Date(int year = 2, int month = 2, int day = 2):_year(year), _month(month){}void Print() const{cout << _year << "-" << _month << "-" << _day << endl;}private:// 声明给缺省值 ->初始化列表int _year = 1;//缺省是备用的,没有值才会用缺省,有值是不用的。int _month = 1;int _day = 1;Time _t;const int _n = 2;
};int main()
{Date d1;d1.Print();return 0;
}

打印:

Time()
2-2-1
  1. 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。
class A
{
public:A(int a):_a1(a),_a2(_a1){}void Print() {cout<<_a1<<" "<<_a2<<endl;}
private:int _a2 = 2;int _a1 = 2;
};int main() {A aa(1);aa.Print();
}

问:

A. 输出1 1

B.程序崩溃

C.编译不通过

D.输出1 随机值

答:D

首先,因为显示写(有参数)了,所以不会使用缺省参数,排除答案有2的选项。

在类里面,先声明了_a2然后声明了_a1

private:int _a2 = 2;int _a1 = 2;

所以先执行,_a2(_a1),用_a1初始化_a2,因为_a1没有初始化,所以是随机值。

然后执行:_a1(a),用a初始化_a1

A aa(1);1给了:_a1(a)里面的a,所以是1


初始化列表总结:

无论是否显示写初始化列表,每个构造函数都有初始化列表。

无论是否在初始化列表显示初始化,每个成员变量都要走初始化列表初始化。

695be9e980eddbdeaf6bde89be2f94f3


1.3 explicit关键字

构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参数的构造函数具体表现:

  1. 构造函数只有一个参数
  2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
  3. 全缺省构造函数
#include<iostream>
using namespace std;
class A
{
public:// 构造函数explicit就不再支持隐式类型转换// explicit A(int a1)A(int a1):_a1(a1){}//explicit A(int a1, int a2)A(int a1, int a2):_a1(a1), _a2(a2){}void Print(){cout << _a1 << " " << _a2 << endl;}int Get() const{return _a1 + _a2;}
private:int _a1 = 1;int _a2 = 2;
};class B
{
public:B(const A& a):_b(a.Get()){}
private:int _b = 0;
};int main()
{// 1构造一个A的临时对象,再用这个临时对象拷贝构造aa1//(1构造一个A的临时对象)构造+拷贝构造(再用这个临时对象拷贝构造aa1)//编译器觉得先构造再拷贝构造太麻烦了,而且两个都是构造,就会优化成直接构造,合二为一了// 编译器遇到连续构造+拷贝构造->优化为直接构造A aa1 = 1;aa1.Print();const A& aa2 = 1;//这里用引用是为了验证“1构造一个A的临时对象,再用这个临时对象拷贝构造aa1”这个观点,因为临时变量具有常性,如果创建了临时变量,引用不加const是会报错的。// C++11之后才支持多参数转化A aa3 = { 2,2 };// aa3隐式类型转换为b对象// 原理跟上面类似B b = aa3;const B& rb = aa3;return 0;
}

2 类型转换

内置类型之间的类型转换:

类型转换是两个对象之间有关联才好互相转换。

算术类型可以互相转换是因为都表示大小。

指针和整型之间可以转换,是因为指针是地址编号,本质上也是表示大小的。

其他类型就基本上不能转了。

自定义类型和内置类型之间的类型转换:(上面的Stack)

借助构造函数进行类型转换。

自定义类型之间的类型转换:(上面的B)

借助构造函数进行类型转换。

应用场景:

class A
{
public:// 构造函数explicit就不再支持隐式类型转换// explicit A(int a1)A(int a1):_a1(a1){cout << "A(int a1)" << endl;}//explicit A(int a1, int a2)A(int a1, int a2):_a1(a1), _a2(a2){}A(const A& aa):_a1(aa._a1),_a2(aa._a2){cout << "A(const A& aa)" << endl;}
/*
1. `A(const A& aa)`:这是拷贝构造函数的声明。它接受一个类型为 `A` 的常量引用参数 `aa`,表示要复制的对象。2. `:_a1(aa._a1), _a2(aa._a2)`:这是构造函数的初始化列表。它使用参数对象 `aa` 的成员 `_a1` 和 `_a2` 的值来初始化新创建对象的成员 `_a1` 和 `_a2`。这意味着新对象的成员变量将拥有与参数对象相同的值。
拷贝构造函数被用于在 Stack 类的 Push 方法中隐式地创建 A 类型对象的副本。
*/void Print(){cout << _a1 << " " << _a2 << endl;}int Get() const{return _a1 + _a2;}private:int _a1 = -1;int _a2 = -1;
};// 存储A类型的数据
class Stack
{
public:void Push(const A& aa){}
//...
};class B
{
public:B(const A& a):_b(a.Get()){}
/*
这个拷贝构造函数接受一个 A 类型对象的常量引用作为参数,并使用这个 A 对象的 Get() 方法的返回值来初始化 B 类对象的私有成员 _b。
1. `B` 类有一个构造函数,它接受一个 `A` 类型的引用作为参数:```cppB(const A& a):_b(a.Get()){}```这个构造函数允许将 `A` 类型的对象转换为 `B` 类型的对象。2. 当你写 `const B& rb = aa3;` 时,编译器会利用 `B` 类的构造函数将 `aa3`(一个 `A` 类型的对象)转换为 `B` 类型的对象。3. 这个转换过程是直接创建了一个 `B` 类型的临时对象,并且 `rb` 成为了这个新创建的 `B` 类型临时对象的常量引用。4. 这个新创建的 `B` 类型对象是临时的,因为它没有一个持久的名称,它只存在于表达式 `const B& rb = aa3;` 的生命周期内。虽然临时对象确实会执行完就销毁,但是这里因为有了`const&`进行指向,所以生命周期会延长了,和引用的生命周期相同了,所以当引用的生命周期结束,这个临时对象才会释放。5. `rb` 是对这个临时 `B` 类型对象的引用,允许你访问这个对象的成员,但你不能修改这个临时对象,因为 `rb` 是一个常量引用。
可以理解为rb最后是B类型的,然后跟那个临时变量一样的值
*/
private:int _b = 0;
};// 内置类型 -> 自定义类型的转换
// 自定义类型 -> 自定义类型转换
int main()
{// 1构造一个A的临时对象,再用这个临时对象拷贝构造aa3// 编译器遇到连续构造+拷贝构造->优化为直接构造A aa1 = 1;aa1.Print();const A& aa2 = 1;int i = 10;double d = i;const double& rd = i;A aa3 = {1, 1};const A& aa4 = { 1, 1 };Stack st;/*A aa5(5);st.Push(aa5);*/st.Push(5);//🌟上面可以用这一行来替代/*A aa6(6,6);st.Push(aa6);*/st.Push({6,6});//🌟上面可以用这一行来替代B b = aa3;const B& rb = aa3;return 0;
}

也可以精简一下:

class A
{
public:// 构造函数explicit就不再支持隐式类型转换// explicit A(int a1)A(int a1):_a1(a1){cout << "A(int a1)" << endl;}//explicit A(int a1, int a2)A(int a1, int a2):_a1(a1), _a2(a2){}A(const A& aa):_a1(aa._a1),_a2(aa._a2){cout << "A(const A& aa)" << endl;}void Print(){cout << _a1 << " " << _a2 << endl;}int Get() const{return _a1 + _a2;}private:int _a1 = -1;int _a2 = -1;
};class B
{
public:B(const A& a):_b(a.Get()){}
private:int _b = 0;
};int main()
{A aa3 = {1, 1};B b = aa3;const B& rb = aa3;return 0;
}

这里的const B& rb = aa3;可以理解为:

  1. B 类有一个构造函数,它接受一个 A 类型的引用作为参数
  2. 编译器会利用 B 类的构造函数将 aa3(一个 A 类型的对象)转换为 B 类型的对象。
  3. 这个转换过程是直接创建了一个 B 类型的临时对象,并且 rb 成为了这个新创建的 B 类型临时对象的常量引用。
  4. 这个新创建的 B 类型对象是临时的,因为它没有一个持久的名称,它只存在于表达式 const B& rb = aa3; 的生命周期内。虽然临时对象确实会执行完就销毁,但是这里因为有了const&进行指向,所以生命周期会延长了,和引用的生命周期相同了,所以当引用的生命周期结束,这个临时对象才会释放。
  5. rb最后是B类型的,然后跟那个临时变量一样的值

3.static成员

  • static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。

  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。

  • static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。

  • 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。

  • 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

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

  • 静态成员也是类的成员,受public、protected、private 访问限定符的限制。

  • 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。

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

实现一个类,计算程序中创建出了多少个类对象?

下面看几段代码理解一下:

代码1:

#include<iostream>
using namespace std;
class A
{
public:A() { ++_scount;}A(const A& t) { ++_scount; }/*~A(){--_scount;}*/int GetACount() { return _scount;}
//private:// 类里面声明static int _scount;
};
// 类外面初始化
int A::_scount = 0;//相当于全局的静态放到静态里面受到限制void Func(A aa){}int main()
{A aa1;A aa2 = aa1;Func(aa1);cout << A::_scount << endl;cout << aa1._scount << endl;return 0;
}

打印:

3
3

代码2:

#include<iostream>
using namespace std;
class A
{
public:A() { ++_scount;}A(const A& t) { ++_scount; }~A(){--_scount;}int GetACount() { return _scount;}
//private:// 类里面声明static int _scount;
};
// 类外面初始化
int A::_scount = 0;//相当于全局的静态放到静态里面受到限制int main()
{A aa1;A aa2 = aa1;Func(aa1);cout << A::_scount << endl;cout << aa1._scount << endl;return 0;
}

打印:

2
2

代码3:

#include<iostream>
using namespace std;
class A
{
public:A(){++_scount;}A(const A& t){++ _scount;}~A(){--_scount;}int GetACount(){return _scount;}
private:// 类里面声明static int _scount;
};
// 类外面初始化
int A::_scount = 0;//相当于全局的静态放到静态里面受到限制void Func(A aa) {}int main()
{A aa1;A aa2 = aa1;Func(aa1);cout << aa1.GetACount() << endl;return 0;
}

打印:

2

代码4:

#include<iostream>
using namespace std;
class A
{
public:A(){++_scount;}A(const A& t){++ _scount;}~A(){--_scount;}static int GetACount(){//i++;//🌟这行代码不注释会报错。因为用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。//静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。return _scount;}
private:// 类里面声明static int _scount;int _i = 0;
};
// 类外面初始化
int A::_scount = 0;//相当于全局的静态放到静态里面受到限制void Func(A aa) {}int main()
{A aa1;A aa2 = aa1;Func(aa1);cout << aa1.GetACount() << endl;return 0;
}

打印:

2

代码5:

#include<iostream>
using namespace std;
class A
{
public:A(){++_scount;}A(const A& t){++ _scount;}~A(){--_scount;}static int GetACount(){//i++;return _scount;}
private:// 类里面声明//static int _scount = 0;//🌟这行不行,因为静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,//静态成员变量不属于某个对象,不走构造函数初始化列表。static int _scount;int _i = 0;
};
// 类外面初始化
int A::_scount = 0;//相当于全局的静态放到静态里面受到限制void Func(A aa) {}int main()
{A aa1;A aa2 = aa1;Func(aa1);cout << aa1.GetACount() << endl;return 0;
}

打印:

2

求1+2+3+…+n

373f48a9aadebea8ea52f591f515e5b6

代码:

class Sum{
public:Sum(){_ret += _i;//第一次_ret+1,第二次_ret+2......++_i;}static int GetRet(){return _ret;}
private:static int _i;static int _ret;
};//_i和_ret属于全局
int Sum::_i = 1;
int Sum::_ret = 0;class Solution {
public:int Sum_Solution(int n) {Sum arr[n];//定义一个Sum类的arr数组,让他的构造函数调用n次return Sum::GetRet();}
};

这个代码放在VS是跑不了的,因为VS不支持变长数组。变长数组是C99支持的。

如果实在想在VS里面用的话,可以把 Sum arr[n];改成 Sum* arr = new Sum[n];,new这个东西就是构造一个n个数的数组,动态开辟,会调用n次构造函数。这个东西在后面的动态内存管理里会学。当然new好了也要释放。


设已经有A,B,C,D 4个类的定义,程序中A,B,C,D构造函数调用顺序为?()

设已经有A,B,C,D 4个类的定义,程序中A,B,C,D析构函数调用顺序为?()

A:D B A C

B:B A D C

C:C D B A

D:A B D C

E:C A B D

F:C D A B

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

答案是:E,B

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

全局对象在main函数之前初始化,所以先是C

然后按顺序是A,B

D是静态局部变量,只有在第一次走到那边的时候才会初始化。所以是D

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

先析构AB,因为CD的生命周期是全局的,所以在main函数结束后析构。

后定义的先析构,所以是BA

然后局部的静态变量会先析构,然后再析构全局的静态变量。

所以是DC


4. 友元

  • 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面。

  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。

  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。

  • 一个函数可以是多个类的友元函数。

  • 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。

  • 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。

  • 友元类关系不能传递,如果AB的友元, BC的友元,但是A不是C的友元。

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

代码1:

#include<iostream>
using namespace std;
// 前置声明,都则A的友元函数声明编译器不认识B
class B;class A
{// 友元声明friend void func(const A& aa, const B& bb);//func 函数在A类里面被声明为友元函数,func可以访问A里所有的成员变量
private:int _a1 = 1;int _a2 = 2;
};class B
{// 友元声明friend void func(const A& aa, const B& bb);//func 函数在B类里面被声明为友元函数,func可以访问B里所有的成员变量
private:int _b1 = 3;int _b2 = 4;
};void func(const A& aa, const B& bb)
{cout << aa._a1 << endl;cout << bb._b1 << endl;
}int main()
{A aa;//在 main 函数中,创建了 A 类和 B 类的对象 aa 和 bb。然后调用 func 函数,并将这两个对象作为参数传递。B bb;func(aa, bb);//当 func 被调用时,它能够访问 aa 的私有成员 _a1 和 bb 的私有成员 _b1,并分别打印它们的值。return 0;
}

代码2:

#include<iostream>
using namespace std;
class A
{// 友元声明friend class B;//class B 被声明为 `class A` 的友元类。这意味着 `B` 类的成员函数可以访问 A 类的私有(private)和保护(protected)成员
private:int _a1 = 1;int _a2 = 2;
};class B
{
public:void func1(const A& aa){cout << aa._a1 << endl;cout << _b1 << endl;}void func2(const A& aa){cout << aa._a2 << endl;cout << _b2 << endl;}
private:int _b1 = 3;int _b2 = 4;
};int main()
{A aa;B bb;bb.func1(aa);bb.func1(aa);return 0;
}

代码3:

func 函数定义在类外部,作为 A 类的友元函数

#include <iostream>
using namespace std;// A 类定义
class A
{// 友元函数声明,声明 func 为友元函数friend void func(const A& aa);private:int _a1 = 1;int _a2 = 2;
};// func 函数定义在类外部,作为 A 类的友元函数
void func(const A& aa)
{// 访问 A 类的私有成员 _a1 和 _a2cout << "From func: A's _a1 = " << aa._a1 << endl;cout << "From func: A's _a2 = " << aa._a2 << endl;}int main()
{A aa;func(aa); // 调用 func 函数,它作为 A 的友元函数可以访问 A 的私有成员return 0;
}

打印:

From func: A's _a1 = 1
From func: A's _a2 = 2
关键字:【C++】7.类和对象(5)

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: