首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++ 类与对象(3)

C++ 类与对象(3)

作者头像
君辣堡
发布2025-12-20 09:06:20
发布2025-12-20 09:06:20
230
举报

这一篇我们来给C++类与对象收尾,这一篇还会补充类的默认成员函数没讲的部分,开始吧

1.类的默认成员函数::取地址运算符重载

1.1const成员函数

C++将const修饰的成员函数称为 const成员函数,const修饰成员函数放到成员函数参数列表的后面     例子:void func(int a)const

const 实际修饰的是该成员函数的隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

比如 const 修饰Date类的Print函数,Print 的this指针将由 Date* const this 变成 const Date* const this  (指针的指向和指向的内容都不许修改)

const修饰的成员函数也有许多好处,一是保证了this指向的值,也就是成员不会被修改,二是可以解决权限放大的问题,比如const 修饰的对象引用了非const的成员函数,这样就导致了权限放大,进而导致了报错。  综合这两点,代码的健壮性得到了提高

然而,也并非什么时候都要加const,比如Date类重载 日期+=天数 函数时,+=函数会使左侧操作数的值修改,这时如果加了const,就修改不了了,导致无法满足函数需求。简单说,判断原则就是:如果函数不修改当前对象(*this),就加const;如果要修改,就不加

总之,const修饰成员函数的核心就是限制this指针的权限,所有不修改this指针的成员函数都应该用const修饰。

下面我来举例子帮助大家进一步理解:

代码语言:javascript
复制
// void Print(const Date* const this) const

void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}

如图,Date类的Print函数仅仅只是打印日期,并不需要修改this指针,所以建议加上 const 。所以实际上我们之前写的日期类Date的实现并不完善,很多函数需要用const修饰。

代码语言:javascript
复制
Date d1(2024, 7, 5);
d1.Print();

const Date d2(2025,11,4);
d2.Print();

如图,d1无const修饰,所以调用Print是权限缩小,可以调用。d2有const修饰,调用Print是权限平移,也没事。但如果Print没用const修饰,那d2再调用Print就是权限放大了,此时调用就会报错。


1.2取地址运算符重载

取地址运算符重载分为 普通取地址运算符重载const取地址运算符重载 ,一般这两个函数编译器自动生成的就够我们用了,不需要去显式实现。除非一些特殊的场景,比如我们不想让别人获取当前类对象的地址,此时就可以显式实现,胡乱返回地址:

代码语言:javascript
复制
class Date
 {
 public :

 Date* operator&()
 {
 return this;
 // return nullptr;
 }

 const Date* operator&()const
 {
 return this;
 // return nullptr;
 }
 private :
 int _year ; 
 int _month ; 
 int _day ; 
 };

如图,正常就是返回this指针(原地址),但如果不希望别人获取地址,可以返回nullptr ,也可以胡乱写一个地址传回去(注意强转为同类型)。      这个东西实际上用处不大,了解就行。


2.深挖构造函数


之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值。其实构造函数初始化还有另一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个逗号分隔的数据成员列表,每个"成员变量"后面跟着一个放在括号内的初始值或者表达式,这么说可能很抽象,我举个例子:

代码语言:javascript
复制
Dateint year = 1, int month = 1, int day = 1)
     :_year(year)
     ,_month(month)
     ,_day(day)
 {}

如图。需要注意,每行不需要加分号“ ;”,还有最后面的一对中括号不可以省略。

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

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

C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。编译器首先选择初始化列表显示初始化的值,其次是声明时的缺省值,最后是随机值(编译器决定)

尽量使用初始化列表初始化,因为你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明的时候给了缺省值,初始化列表会使用这个缺省值初始化。如果内置类型没有给缺省值,也没有显式在初始化列表初始化,那么是否初始化就取决于编译器,一般我们默认是随机值。对于没有显示在初始化列表初始化的自定义类型会调用他自己默认构造函数,如果没有,则编译报错。

初始化列表按照成员变量在类中的声明顺序初始化,跟成员在初始化列表的先后顺序无关。建议声明顺序和初始化列表顺序保持一致,方便解读代码

如图,无论如何,成员变量一定会走初始化列表。所以我们建议尽量使用初始化列表初始化

内置类型可以在初始化列表初始化,也可以在函数体内赋值进行初始化,也可以混着用:

代码语言:javascript
复制
Date(int year = 1, int month = 1, int day = 1)
    :_year(year)
    ,_month(month)
{
    _day = day;
}

有3个例外:

代码语言:javascript
复制
Time _t;       // 没有默认构造
int& _ref;     // 引⽤
const int _n;  //const

如图,这三个必须在初始化列表初始化。 这三个成员有个特点:都必须在定义的时候初始化.

所以统一一下,大家都决定,都在初始化列表进行初始化更好。  代码演示:

代码语言:javascript
复制
Date(int& x, int year = 1, int month = 1, int day = 1)
 :_year(year)
 ,_month(month)
 ,_day(day)
 ,_t(12)
 ,_ref(x)
 ,_n(1)
 {}

除了int& _ref  必须在初始化列表显式初始化,因为其引用传参的特殊性,导致无法给缺省值,其他两个也可以在声明时给缺省值:

代码语言:javascript
复制
Private:
Time _t = 1;
const int _x = 1;

之前不是说了“这三个必须在初始化列表初始化”了吗? 是的,没错,声明时的缺省值是 “fallback 方案”—— 仅当初始化列表未显式初始化时生效。若在初始化列表显式初始化了,那么缺省值会被直接忽略,不会报错。

这里注意,因为_ref是引用传参,所以参数列表中x必须是引用,即int& x,不然传值传参,等初始化列表结束后,x销毁,_ref就变成了野引用,类似野指针。

如果这三个没在初始化列表进行初始化就会报错:

代码语言:javascript
复制
// error C2512: “Time”: 没有合适的默认构造函数可⽤
// error C2530 : “Date::_ref” : 必须初始化引⽤
// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象

自定义类型没有自己的默认构造函数,就会编译报错,若MyQueue中的Stack没了默认构造函数、此时MyQueue写的默认构造函数也不可以直接对Stack对象初始化。(Stack对象已经被定义出来,并不是在定义时初始化,所以不行),那么初始化列表就可以解决这个问题:

代码语言:javascript
复制
MyQueue(int n = 1)
    :_st1(n)
    ,_st2(n)
{}

下面我们做道题:

下⾯程序的运⾏结果是什么()

A. 输出 1 1

B. 输出 2 2

C. 编译报错

D. 输出 1 随机值

E. 输出 1 2

F. 输出 2 1                          //截取部分关键代码:

代码语言:javascript
复制
 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();
}

如图,main函数内定义了一个A aa,并传参一个 1 给带参构造函数,在带参构造函数内,初始化列表看似先是 显式先初始化了_a1, 然后初始化了_a2, 但由于初始化顺序是跟随着声明顺序的,所以应该是进行 _a2(_a1),此时_a1还未显式在初始化列表初始化,所以默认调用缺省值,所以_a2 的值是2,然后进行 _a1(a),  a为1,所以_a1 的值是 1。最后打印,所以答案是 1 ,2    ,这题选择 E 。

到此,对于类的默认构造函数就补充完毕。


3.类型转换

C++支持内置类型隐式类型转换为 类 类型对象,需要相关内置类型为参数的构造函数

比如 A aa = 5 ;  则需要A类有一个参数为 int 的带参构造 

构造函数前面加 explicit 就不再支持隐式类型转换,这就是ex12

plicit关键字的用处。(后面有用)

类 类型的对象之间也可以隐式类型转换,同样需要相应的构造函数支持(如上面的例子)

如上图,隐式类型转换是会产生临时对象的,i 生成了一个临时对象,然后临时对象拷贝给 d

这图也类似,不过这是整型通过构造函数的参数构造了一个临时A对象,然后临时对象拷贝构造给给了A a2。这个过程会被部分编译器优化为直接构造

图中是不同编译器的选择,上面的实现了优化,而下面的画圈部分,明显多了一次拷贝构造的调用。编译器做出优化本质上是为了提高效率,对语法进行了更优的思考,既允许简洁的隐式转换语法,又通过编译优化避免了不必要的性能损耗(减少了临时对象的创建和拷贝)。如今主流编译器都会进行优化。

对于自定义类型的隐式类型转换,也是类似的,需要另一个类有相关自定义类型为参数的构造函数

如图,Stack的构造函数参数为A类,这是想 从A隐式类型转换为Stack 的前提条件

看右边,我们期望在Stack的Push函数中插入一个A类型,首先创建一个栈st1 ,然后以3为参数构造一个A a3 ,最后直接用a3 作为参数插入。但是学了隐式类型转换,我们直接写成st1.Push(3) 就行。 3 通过A的构造函数 生成一个临时对象A ,然后A引用传参给Push函数(这个临时对象会在Push函数结束时被销毁,不用管),这就是隐式类型转换

这说明如果B类的带参构造函数的参数为const A& a (A类的构造函数参数为整型),那么构造B类时,可以直接用整型作为参数,也可以传A类对象。  const B& ref 需要引用时,可以传A类对象也可以传B类对象,甚至传整型。

同样,也可以这样:

代码语言:javascript
复制
void func(const A& a)
{
    //内容
}

int main()
{
	A a1(1);
	func(a1);
	func(1);
	return 0;
}

如果有函数需要引用传参一个类 ,而该类的带参构造函数参数为int类型, 那直接整型就行,原理相同,隐式类型转换。

补充一点,如果A类的带参构造函数参数是两个int(int a,int b) ,那需要这么写:

代码语言:javascript
复制
A(int a,int b)
{
    _a = a;
    _b = b;
}

    A a1(2,3);
    A a2 = {2,3};
    const A& ref3 = {2,3};
    st1.Push({2,3});

如图,双参数的写法就是这样,如果需要走隐式类型转换,那就用大括号括起来,逗号分隔


4.static成员

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

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

同样,用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针

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

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

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

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

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

代码语言:javascript
复制
class A
{
private:
	int _a1 = 1;
	int _a2 = 2;
public:
	static int _count;
};

//注意,一定要指明类域!
int A::_count = 1;

int main()
{
	A aa1;
    A* ptr = nullptr;
	cout << ptr->_count << endl;
	cout << aa1._count << endl;
	cout << A::_count << endl;
	return 0;
}

如图,static成员变量声明在public时,可以通过图中main函数的几种方法进行访问。

因为static成员变量的初始化不走构造函数的初始化列表,所以得在类外进行定义初始化。

static成员变量声明在private时,以上方法都不行,只能通过该类的public函数间接访问。

比如在public 声明定义(定义可分离)一个Get_count函数,然后传值传参给一个整型变量:

代码语言:javascript
复制
static int Get_count()
{
    return _count;
}

Get_count函数也用static修饰 是为了让访问静态成员变量的接口在逻辑上与静态成员的特性保持一致,同时避免依赖对象实例,简化调用方式(如果不static修饰,想调用就得再实例化一个对象来间接调用,加了只需要指定类域A::Get_count),是 C++ 中 “静态成员配静态接口” 的经典设计范式。

在默认构造,析构,拷贝构造加上一句 “++_count” (调用一次对应函数,_count自增一次),也可以随时检查对应函数使用次数,是否被使用。这个可用于检查隐式类型转换过程中是直接构造。

根据这个方法,我们可以做一道经典题目:求1+2+3+...+n   (牛客网)

代码语言:javascript
复制
class Sum
{
public:
    Sum()
    {
        _ret+=_i;
        ++_i;
    }
   static int Get_ret()
    {
        return _ret;
    }
private:
    static int _ret;
    static int _i;
};
    int Sum::_i = 1;
    int Sum::_ret = 0;

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

代码如图。声明两个static修饰的整型变量然后声明,这里需要知道:定义了一个Sum类的数组arr[n],n的长度是多少,就会调用几次Sum类的默认构造函数。基于此,我们直接在他构造函数内利用两个static整型变量进行运算,然后利用Get_ret 函数返回 _ret (static整型变量声明在private)

正好结合以前的知识,我们来做两道选择题:

我先把答案遮起来。现在我们来分析一下第一题。首先问的是ABCD的构造函数调用顺序,那么这题很简单,先运行到的先构造,按照顺序直接就是CABD,全局的优先构造,然后到main函数,继续按照顺序ABD,所以这题选择:   E

对于main函数中的静态对象D,很多人可能有疑问,为什么不先构造D?   其实局部static对象,都是在第一次运行的定义位置时,才初始化的。

看看第二题,这里问的是ABCD的析构顺序,那全局对象C的析构会在main函数内的局部对象ABD之后,因为存储在全局数据区(静态区),static D也是存储在静态区 ,静态区存储的对象生命周期贯穿整个程序。但因为比全局对象C晚定义,所以先析构。因此C是最后析构的。其次,局部static对象在局部是最后析构的,因为AB是存储在栈区的,栈区对象的生命周期随函数结束而结束先于静态存储区对象析构。 B后定义的先析构,所以B先于A析构。 综上,析构顺序是 BADC

所以答案选择 B

C++类与对象(3)到此结束。下一篇我将结束类与对象的知识,请大家多多支持,有误请指出,谢谢大家!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.类的默认成员函数::取地址运算符重载
    • 1.1const成员函数
    • 1.2取地址运算符重载
  • 2.深挖构造函数
  • 3.类型转换
  • 4.static成员
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档