C++基于对象进行编程——类和对象(三)

本文深入探讨C++中的构造函数初始化列表的重要性及其语法,解释为何需要初始化列表,并介绍explicit关键字的使用。此外,文章还讲解了static成员变量与函数的概念及应用场景,友元函数与类的使用方式,以及内部类的特性和封装思想。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、构造函数再探——初始化列表

1 在构造函数体内赋值

  我们以前写构造函数用的方法其实都算是在构造函数体内赋值,意思是说我们在成员函数内的时候,对内置类型来说,这些变量已经在地址空间上被写好了随机值,而不是我们直接在这块地址空间上写上我们想给它的值,如Date类:

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

  所以这并不能算初始化,只能算赋值,而非定义。

  定义的格式应该类似这样:int _year = 3;int _month = 4; int _day = 3;.而非构造函数中那样,构造函数中的其实可以换为this指针格式:this->_year = year;this->_month = month; this->_day = day;

  那我们不禁想问,成员变量的定义在哪里呢?

  成员变量的定义其实在实例化对象的一瞬间,编译器实例化对象的时候就会定义成员变量并且不管不作处理,值成了那些随机值,后面我们在构造函数体内的行为都只能算赋值,所以用的也是operator=;

  C++中是允许我们自己来在对象定义时赋好成员变量初值的,即类似int _year = 2022;这种过程,这个语法就叫做成员初始化列表。

2 初始化列表的语法

  语法:

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

3 为什么要有初始化列表?

  const变量必须在定义时初始化,也就是第一次写入内存时初始化,并且只有这一次初始化机会,不然会报错,如下:

  那假如类中有一个成员变量是const,下面的实验会表明,当进入构造函数体内后到,成员变量已经经过了第一次写入内存(毕竟构造函数只是在给已经定义出来的类对象成员赋初值),再在构造函数体内没法为const成员变量赋值了,如下:

  C++提供的初始化列表可以解决这个问题,初始化列表相当于这些变量定义(即第一次写入内存)的地方

  C++提供的这两种方法可以混用,更推荐使用成员变量初始化列表,可以应对更多问题,所有必须定义的时候就初始化的成员变量都需要把初始化放到成员初始化列表中。

  除了const变量,还有引用和没有默认构造函数的自定义类型的成员变量的初始化需要放到成员初始化列表中。

  引用在定义的时候必须初始化,且之后不允许改自己去引用别的,所以如果我们不写成员初始化列表,引用变量就已经在成员初始化列表中不知道要去饮用水,类似于这种情况int& j;,然后就会报错。

  所以引用类型要放到成员初始化列表里,下面是一个小实验

  放到初始化列表就好了:

  如果类中的自定义类型在编译器给我们定义的时候,如A _a;的时候,没有默认构造函数可以用,也会报错。

  详细分析是在进入构造函数体前,也就是成员初始化列表(我们不写也会有)中,在编译器定义这个成员变量的时候,即A _a;这一过程,由于没有参数,所以调用默认构造函数,如果此时自定义类型没有默认构造函数,就会报错,比如下面这个例子:

class A
{
public:
    A(int a)
    {
        _a = a;
    }
private:
    int _a;
};
class Date
{
public:
    Date(int year, int month, int day, int D, int x)
        :_year(year),
        _month(month),
        _day(day),
        _D(D),
        _x(x)
    {
        a = A(1);
    }
private:
    int _year;
    int _month;
    int _day;
    const int _D;
    int& _x;
    A a;
};

  这种情况下,我们就要自己显示的调用构造函数,且放到初始化列表中,如下:

  另外,编译器默认生成的构造函数,对自定义类型调用默认构造函数的调用就是在在类似成员初始化列表的过程中调用的。

  总结:

  • 初始化列表是成员变量定义的地方,进入构造函数体内时已经是成员变量定义完了,赋值的地方了;

  • 对于const变量、引用类型、没有默认构造函数的自定义类型成员变量必须在初始化列表初始化,因为它们必须在定义的时候初始化;

  • 对于像其他类型的变量,在哪初始化都行,因为它们有不必定义的时候就要初始化。

  考虑到构造函数在定义后赋值会出现那么多问题,建议都用初始化列表初始化,并且实际上在构造函数体内初始化速度较初始化列表更慢一些,例子如下:

class A
{
public:
	A(int a = 1)
	{
		_a = a;
		cout << "A(int)" << endl;
	}
	A& operator=(const A& a)
	{
		_a = a._a;
		cout << "A& operator=" << endl;
        return *this;
	}
	A(const A& a)
	{
		_a = a._a;
		cout << "A(const A& a)" << endl;
	}
private:
	int _a;
};
class D
{
public:
	D(A aa)
	{
		_aa = aa;
	}
private:
	A _aa;
};

int main()
{
	A a;
	D d(a);
}

  分析一下这里发生的事情。首先,就算我们没写初始化列表,定义时也会编译器也会在定义自定义类型时,即A _a;过程调用自定义类型的默认构造函数。

  然后在上面的代码中,第一次A(int)是定义a,然后构造d时,先把a传参给D的构造函数中的aa,调用了一次拷贝构造函数传参,所以这里打印了一次拷贝构造函数A(const A& a);

  然后打印的A(int)就是_aa在定义的时候编译器调用的A的默认构造函数,接下来调用了赋值重载操作符去把传过来的aa赋值给_aa

  而如果使用成员初始化列表,这一过程就快多了,直接在定义的时候调用了拷贝构造函数,而不像写在构造函数体内时一样先调用默认构造函数定义对象,然后用operator=给对象赋值,下面这个粒子可以看出成员初始化列表是直接调用了只调用了拷贝构造函数定义对象:

class A
{
public:
	A(int a = 1):_a(a)
	{}
	A(const A& a) :_a(a._a)
	{
		cout << "A(const A&)" << endl;
	}
private:
	int _a;
};
class B
{
public:
	B():_A(),_m(1)
	{}
	B(const A& a, int m):_A(a),_m(m)
	{}
private:
	A _A;
	int _m;
};

int main()
{
	A a(12);
	B b1(a, 13);
}

  还有一个注意点:成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

  如这道例题:

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();
}
//所以会先以_a1初始化_a2,一开始是_a1是随机值,然后以a初始化_a1,所以_a1是1,a2是随机值

  所以为了避免出现这种不自然的情况,我们尽量把初始化列表的顺序和成员变量的定义顺序弄成一样的。

4 explicit关键字

  构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有隐式类型转换的作用,如下:

  C++中允许这样的事情发生:

class Date
{
public:
    Date(int year):_year(year)
    {}
private:
    int _year;
};

int main()
{
    Date d = 2022;
    //相当于Date d = Date(2022);
}

  一个int赋值给了Date,这在C++中被称为隐式调用构造函数,也称隐式类型转换,隐式类型转换是意义相近的类型可以互相转换。

  这里发生的过程是先构造一个临时变量,再拷贝给你。

  这里是先构造了一个Date(2022),然后拷贝构造d

  不过这里像上一篇C++博客里讲的那样,C++编译器中,连续的构造函数调用可能会被优化,合二为一,直接以2022year参数构造d

  这里就优化成了1次构造,如下:

  如果你不想让这样的隐式调用构造函数的事情发生,因为这毕竟很不自然嘛.

  就可以在构造函数前面加关键字explicitexplicit的意思是显示、直接的、明了的,这个关键字会阻止隐式调用,加在普通构造函数后面,会发生这种情况,它阻止了这种隐式调用构造函数。

  拷贝构造函数也可以加explicit,它会阻止如Date d2 = d1;这样的隐式调用

class Date
{
public:
    Date(int year = 1) :_year(year)
    {
        cout << "Date(int)" << endl;
    }
    explicit Date(const Date& d)
    {
        _year = d._year;
    }
private:
    int _year;
};

int main()
{
    Date d = 2022;
    //Date d1 = d;
    //相当于隐式调用拷贝构造函数 被explicit禁止
    Date e;
    e = Date(d);
    //这样是可以过的 显示调用拷贝构造函数生成临时对象然后赋值给e
}

二、static成员

1 static成员变量

  以一个计数器引出static成员,如果我们想统计一下调用了多少次构造函数,需要一个计数器,但是每个实例化的对象的count不会累加,不同的对象不会再同一个变量中定义,这就引出了static成员变量,它属于整个类域,而不是每个对象都持有不同的一个。

class A
{
public:
    A(int a):_a(a)
    {
        ++_cnt;
    }
    A(const A& a):_a(a._a)
    {
        ++_cnt;
    }
private:
    int _a;
    static int _cnt;
};

//初始化 可以不加static 但是要指明类域
int A::_cnt = 0;

2 static成员函数

  如果我们要在类外访问静态成员变量,有两种方法:

//共有
cout << A::_cnt << endl;//直接指定类域
cout << a._cnt << endl;//通过对象.访问到
//私有
//只能提供公开接口访问到了
//引出static成员函数

  static成员函数的特点是没有this指针,只能访问静态成员变量和静态成员函数。

class A
{
public:
    A(int a = 1) :_a(a)
    {
        ++_cnt;
    }
    A(const A& a) :_a(a._a)
    {
        ++_cnt;
    }
    static int Getcount()
    {
        return _cnt;
    }
private:
    int _a;
    static int _cnt;
};

//初始化 可以不加static 但是要指明类域
int A::_cnt = 0;

int main()
{
    A a;
    cout << a.Getcount() << endl;
    cout << A::Getcount() << endl;
}

例题:禁止使用循环、递归、公式、位运算、乘法、除法、计算1+2+…+n.

  这里就可以使用静态成员变量和静态成员函数.

class sum
{
public:
    sum()
    {
        ++_n;
        _ret += _n;
    }
    static int GetRet()
    {
        return _ret;
    }
private:
    static int _n;//静态的计数器
    static int _ret;
};
int sum::_n = 0;
int sum::_ret = 0;

int main()
{
    int n;
    cin >> n;
    vector<sum>(n);
    cout << sum::GetRet() << endl;
}

三、C++11成员初始化的补丁

  我们都知道,不写构造函数时,编译器自己生成的构造函数,它对内置类型不处理,对自定义类型调用默认构造函数,这非常不好,你干脆在我不写构造函数的时候,把内置类型一并处理了,这样我就不用再搞那么多麻烦了,C++11对此打了个补丁:

class A
{
public:
private:
    int _a1 = 0;//这里不是初始化
    //试想,对象都没创建呢 你初始化什么空间?
    //这里只是声明
    //严格来说 这个 = 0给的是缺省值
};

  如果你在初始化列表没有对成员变量初始化,它就会用缺省值初始化,这个初始化的行为是在进入构造函数体内之前的,我们可以验证如下:

class A
{
public:
	A(int a)
	{
		_a = a;
	}
private:
	int _a = 2;
};

int main()
{
	A a(3);
}

  可以通过几步单步调试看一下:

  这里其实是想执行成员初始化列表:

  执行直接给_a传缺省值

  然后才进入构造函数体内。

  构造函数体内赋值。

  并且有很多东西都可以在这里通过缺省提供,数组可以给初值,int*可以接收malloc开辟内存的首地址,也可以接收函数调用的返回值。

  但是静态的成员变量不可以用这种方法处理,因为这里给的是每个对象的成员变量的缺省值,是传给构造函数的缺省值,是给构造函数的初始化列表用的,构造函数并不能处理静态的成员,因为这个是整个类共享一份,所以必须在类外全局位置定义初始化。

四、友元函数

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

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

1 友元函数

  我们已经在上篇C++中使用了友元函数来重载operator<<operator>>,这里总结一下它的特点:

  • 友元函数不能用const修饰,因为它是通常是全局函数,没有this指针,const修饰谁呢?
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用和原理相同

2 友元类

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

  • 友元关系是单向的,不具有交换性。

    比如如果声明A是B的友元类,A的成员函数都可以访问B的成员,但是B的成员函数没办法访问A的成员。

  • 友元关系不能传递

    如果B是A的友元,C是B的友元,则不能说明C时A的友元。

class B
{
public:
	friend class A;
	void getA(const A& a);
private:
	int _b;
};
class A
{
public:
	void getB(const B& b);
private:
	int _a;
};

void B::getA(const A& a)
{
	cout << a._a << endl;
}
void A::getB(const B& b)
{
	cout << b._b << endl;
}

五、内部类

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

  注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类,如下例中B并不是在A里面的,这里不要和继承搞混掉。

  外部类对内部类没有任何优越的访问权限,即外部类不可以访问内部类的private成员,和内部类的用户是一样的。

  但是内部类是外部类的友元类。内部类可以通过外部类的对象参数来访问外部类中的所有成员。

  但是外部类不是内部类的友元。

class A{
public:
    class B
    {
    public:
        void GetA(const A& a)
        {
            cout << a._a << endl;
        }
    private:
        int _b = 1;
    };
private:
    int _a = 3;
};

  通俗来说,内部类和一个定义在全局的类是一样的,除了两个点(假设B是A的内部类)不同:

  • B受外部类A的域的限制,定义B对象需要这样写A::B b;(前提是B处于A的public修饰符,如果是private那在类外无法访问)
  • 内部类B天生是A的友元,B可以随便访问A的成员,但A不是B的友元,A不能随便访问B的成员。

  B并不真实的在A里面,只是定义在A的类域中且B是A的友元,所以上图里头A的大小仍是4个字节。

  考虑到内部类可以访问外部的private成员,我们可以修改上面的那个1+2+…+n的做法如下:

class Solution {
public:
    int Sum_Solution(int n) 
    {
        Sum s[n];
        return _ret;
    }
    class Sum
    {
    public:
        Sum()
        {
            //Sum是内部类,天生是外部类的友元
            //可以访问到外部类的成员
            //所以把计数器放到外部类的private去
            //保留了实现细节而且内部类可以访问它
            ++_n;
            _ret += _n;
        }
    };
private:
    static int _ret;
    static int _n;
};
int Solution::_ret = 0;
int Solution::_n = 0;

六、封装和面向对象的思想的一个小例子

我们知道封装是封锁内部实现细节不给类的用户显示,为什么要这样做呢,试想我们在坐飞机,机场安检、广播通知登机,工作人员负责开飞机和服务用户,我们不需要了解机场的广播是怎么工作的、也不需要了解飞机是怎么实现飞行的,但是公开给我们的这些接口,机场安检、广播通知登机、工作人员的服务就能使大家有条不紊的完成出行;

是想,如果机场没有做任何管理措施呢,也就是不封装的思想。乱登机,登错机,行窃预兆(不是),这些东西会一并出现,会显得很混乱,这里就体现了封装的好处了。

所谓基于对象的含义就是以对象来模拟现实世界中的实体,以现实中实体所属的抽象类别的公共特征来定义对象的类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值