重温C与C++之构造函数

简介: C++进阶之构造函数

写在前面

相信做过Java、C++或者其他面向对象语言开发的朋友们一定对构造函数这个概念不陌生。以前初学C++的时候笔者看过几次《C++ Primer》这本书,但是每次都是走马观花式的快速阅读,
每次浏览完之后内心就会冒出两个字:就这?现如今回想起来真是图样图森破

学习最忌讳的就是心急如焚,砍柴不磨刀,所谓欲速则不达,一步一个脚印才能走得更稳。

由问题开始

下面我们就从几个问题出发,加深一下对C++中构造函数的了解:

1、构造函数初始化与赋值的问题

以下的这两个写法有什么区别?

class Person {
public:
    Person(const string name, int age);

private:
    string name;
    int age;
};

// 第一种写法
Person::Person(const string name, int age) {
    this->name = name;
    this->age = age;
}

// 第二种写法
Person::Person(const string name, int age):name(name),age(age) {
    
}

在这个例子中第二种写法是使用构造函数初始值的写法,第一种写法虽然合法,也没有错误,但是并不是合理的写法,并不推荐。

那么这两种写法有什么区别呢?
第一种写法会经历先初始化,再赋值这么两个过程;而第二种写法则是直接初始化数据成员一步到位。所以这里面会存在一个效率的问题,第二种写法的效率更高。

我们再看一个例子,如果我们把类的成员使用const修饰呢,结果会怎样?

class Person {
public:
    Person(const string name, int age);

private:
    string name;
    const int age;
};

// 第一种写法,编译报错
Person::Person(const string name, int age) {
    this->name = name;
    this->age = age;
}

// 第二种写法
Person::Person(const string name, int age):name(name),age(age) {

}

我们发现第一种写法行不通了,不能编译通过了,这是因为age被const修饰了,必须在初始化时赋值,所以第一种写法就不行了,由此看出使用构造函数初始值的写法更加规范,更加安全。

建议:在《Effective C++》一书中的第4条"确定对象被使用前已先被初始化"中也强调了绝对必要使用构造函数初始值

2、成员变量的初始化顺序

如下例子,如果外部调用Point对象的getX方法,能拿到正确的值吗?答案是不能的,因为成员x比成员y先初始化。

class Point {

public:
    Point(int x, int y);

    int getX() const{
        return x;
    }

    int getY() const{
        return y;
    }

private:
    int x;
    int y;
};

// 本意是把 yVal的值赋值给成员变量y,然后把成员变量y的值赋值给成员变量x
Point::Point(int xVal, int yVal):y(yVal),x(y) {

}

一般按照我们常规的思维,我们在构造函数中先写了y,再x,那应该是写初始化y,再初始化x吧?然而事实并不是这样子的。

起始构造函数初始值是有一定的规则的:

构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,
然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。

所以上面构造函数的写法中虽然y出现在了x的前面,但是在成员变量声明的时候是先声明了x的,所以初始化的时候是先初始化了x,但是把一个未经初始化的y赋值给了x,那肯定是不能成功赋值的,
所以通过getX方法获取到的值也就不是你想要的那个值了。

3、对于继承而来的派生类的成员初始化顺序是怎么样的呢?

尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。和其他创建了基类对象的代码一样,派生类也必须使用基类的构造函数来初始化它的基类部分。
首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

3、委托构造函数的执行顺序

所谓委托构造函数就是构造函数相互调用。

当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。
如果受委托的构造函数体恰好是空的。假如函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。

4、构造函数异常如何捕获

处理构造函数初始值异常的唯一方法是将构造函数写成函数try语句块。

5、如何让类不能在栈内构造

笔者查了下网上的资料说大概就是说将构造方法私有化,并且将拷贝构造函数私有化就能禁止类的对象在栈内构造了。笔者测试了一下其实这并不严谨,这样的做法只能做到在类的外部禁用了栈内初始化,
在类的内部依然可以使用栈的方式构造对象,比如一下例子:

class Data {
public:
// 在类的内部依然可以使用栈的方式构造
    Data create() {
        Data data = Data();
    }

private:
    Data();
    Data(const Data &data) {

    }

};

经过笔者的测试,私有化构造函数,再加上使用delete关键字移除拷贝构造函数即可实现禁用类在栈内构造的功能:

class Data {
public:
    // 不能在栈内构造,编译会报错
    Data create() {
        return Data();
    }

private:
    Data();
    Data(const Data &data) = delete;
};

但是这种做法实在是太过了,而且笔者笔者才疏学浅,也不知道这种做法会不会造成什么隐藏的坑,如有高手,请赐教。

《More EffectiveC++》一书中第27条:要求(或禁止)对象产生与heap之中,提到将构造函数和析构函数私有化即可达到禁止对象在栈内定义的目的。
但是这个做法太过了,比较好的办法是让析构函数r成为 private,而构造函数仍为 public。

6、如何让类不能在堆内构造对象

使用new在堆内构造对象主要会调用构造函数以及new运算符这两个步骤,所以我们只要把运算符new移除即可:

class Data {
public:
    Data();
    // 重载new运算符,禁止使用new在堆内构造对象
    void* operator new (size_t size) = delete;
};

然而笔者发现,虽然这样能够禁用new在堆内构造对象,但是我们知道使用 malloc 也能在堆内分配对象,只是使用 malloc 不会调用类的构造函数而已,所以类内的所有成员都需要自己手动初始化,
那么有没有办法把malloc也禁用掉呢?笔者并不知晓,恳请高手赐教。。。

在《Effective C++》一书中第06条有提到为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。

所以为了达到某个类只能在堆内或者只能在栈内构造的目的可以参考这一条。

总结一下

1、谁先声明谁先初始化,与构造函数中出现的顺序无关;

2、初始化值中的相关调用比构造函数中的函数体优先执行;

3、在派生类中首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

结语

不得不感叹一下,C++真是一门博大精深的语言,你学得越多,你不知道的就越多。

学C++三年,口出狂言
再学三年,不敢妄言
又学三年,沉默寡言

就笔者而言,我觉得学习是一个实践的过程,尤其是对于编程类的学习。看博客、看书、看文章只能解决你当时的疑惑,并不能在我脑海中留下根深蒂固的印象,如果不去实践证明、不去总结归纳的话,很快就会忘记了,过不了多久其实和没有学过差不多。

共勉!!!

关注我,一起进步,人生不止coding!!!

目录
相关文章
|
15天前
|
编译器 C++
C++的基类和派生类构造函数
基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。 在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。 这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。 下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:
17 1
|
2月前
|
安全 编译器 C++
C++一分钟之-构造函数与析构函数
【6月更文挑战第20天】C++中的构造函数初始化对象,析构函数负责资源清理。构造函数有默认、参数化和拷贝形式,需注意异常安全和成员初始化。析构确保资源释放,避免内存泄漏,要防止重复析构。示例代码展示了不同构造函数和析构函数的调用情况。掌握构造和析构是有效管理对象生命周期和资源的关键。
32 2
|
1月前
|
编译器 C++
【C++】详解构造函数
【C++】详解构造函数
|
2月前
|
存储 编译器 C++
【C++】类和对象④(再谈构造函数:初始化列表,隐式类型转换,缺省值
C++中的隐式类型转换在变量赋值和函数调用中常见,如`double`转`int`。取引用时,须用`const`以防修改临时变量,如`const int& b = a;`。类可以有隐式单参构造,使`A aa2 = 1;`合法,但`explicit`关键字可阻止这种转换。C++11起,成员变量可设默认值,如`int _b1 = 1;`。博客探讨构造函数、初始化列表及编译器优化,关注更多C++特性。
|
2月前
|
存储 编译器 C语言
【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)
C++类的六大默认成员函数包括构造函数、析构函数、拷贝构造、赋值运算符、取地址重载及const取址。构造函数用于对象初始化,无返回值,名称与类名相同,可重载。若未定义,编译器提供默认无参构造。析构函数负责对象销毁,名字前加`~`,无参数无返回,自动调用以释放资源。一个类只有一个析构函数。两者确保对象生命周期中正确初始化和清理。
|
3月前
|
C++ Linux
|
2月前
|
编译器 C语言 C++
【C++】:构造函数和析构函数
【C++】:构造函数和析构函数
33 0
|
3月前
|
编译器 C++
C++的基类和派生类构造函数
在 C++ 中,类的构造函数不能被继承,但基类的普通成员函数可以在派生类中访问。派生类必须通过其构造函数初始化继承的成员变量,由于私有成员变量无法直接初始化,因此需要在派生类构造函数中调用基类的构造函数来完成。示例代码显示了如何在派生类构造函数中调用基类构造函数,确保正确初始化。构造函数的调用顺序遵循自顶向下、从基类到派生类的规则,且只能调用直接基类的构造函数。如果基类没有默认构造函数,而派生类未指定构造函数调用,会导致编译错误。
38 4
|
3月前
|
编译器 C++
C++程序中的派生类构造函数
C++程序中的派生类构造函数
33 1
|
2月前
|
程序员 编译器 C++
C++中的构造函数以及默认拷贝构造函数
C++中的构造函数以及默认拷贝构造函数
13 0