12.3 构造函数

1.构造函数

构造函数的作用是保证每个对象都被正确的初始化,只要创建对象就一定会调用构造函数。构造函数是与类同名且没有返回值的成员函数。和普通函数的另一个区别是在形参右侧可以提供一个初始化列表,用于初始化数据成员的值。

构造函数的执行分为两个阶段: 1. 初始化阶段,2.计算阶段。 初始化阶段由初始化列表完成,计算阶段由构造函数的函数体完成。初始化阶段会初始化在初始化列表中定义的数据成员,不在初始化列表中定义的成员使用变量初始化规则初始化。

变量初始化规则: 类类型变量使用默认构造函数初始化,内置类型或复合类型,全局作用域初始化为0,局部作用域不初始化

结合初始化列表和变量初始规则可知,初始化列表里的变量,以及类类型的变量,在初始化阶段已经初始化并写入数据,如果在计算阶段重新赋值,会导致重复写入,对性能有负面影响。

1.合成构造函数

用户没有自定义构造函数时,编译器会合成了默认构造函数。数据成员用变量初始化规则做初始化,。我们来看一个简单的示例

int count;
using std::cout;
using std::endl;


class Person {
public:
    std::string name;
    unsigned age;
    bool male;
};

int main() {
    Person p;
    cout << "count:'"<<count<<"',name:'"<<p.name << "',age:'" << p.age << "',male:'" << p.male << "' " << endl;
}

程序输出如下

可以看到,类类型的数据成员调用默认构造函数初始化,内置或复合类型的成员,全局作用域count被初始化为0;局部作用域的age、male没有被初始化,值是未定义的。

使用合成的构造函数,内置类型和复合类型的数据成员没有正确的初始化,所以只要有这类数据成员,推荐自定义构造函数

没有默认构造函数的类类型数据成员、const数据成员、引用类型的数据成员,强制自定义构造函数,用初始列表完成初始化。

2.默认构造函数

当类中有没有默认构造函数的类类型数据成员、const数据成员、引用类型的数据成员时,合成的构造函数不再适用。我们就需要自定义构造函数,而这里头默认构造函数的作用最大。

没有形参或者所有形参都有默认实参的构造函数,称为默认构造函数。上面的合成构造函数就是默认构造函数的一种情况。定义对象时没有指定初始化式,会调用默认构造函数完成初始化。为了让上面的Person类里的age、male也正确的初始化,我们做如下修改

class Person {
public:
    std::string name;
    unsigned age;
    bool male;
    Person() : name("Unknown"), age(0), male(true)  {}
    
};

构造函数形参右侧,:和左大括号{之间的内容叫做初始化列表,格式是数据成员名称(数据成员类型构造器实参),多个字段用逗号隔开。通过初始化列表,我们将name初始化为Unknown字符串,age初始化为0,male初始化为true。

我们可以定义有任意形参的构造器,只要给所有形参都默认实参,这个构造器依然是默认构造器

Person(const string& name = "Unknown", unsigned age = 0, bool male = true) : name(name), age(age), male(male) {}

这两个构造函数不能同时出现,如果同时出现,调用默认构造函数的时候,编译器无法确定调用哪个实现。

没有默认构造函数的影响

如果我们在类内定义了任意的构造函数后,编译器就不会再给我们生成合成构造函数。比如下面这个类定义就是没有默认构造函数的。

class Person {
public:
    const std::string name;
    unsigned age;
    bool male;
    Animal animal;
    Person(std::string name) : name(name), animal("cat"), age(0), male(true) {}
};

特定情况下默认构造函数是由编译器调用的,没有默认构造函数意味着这些场景下无法使用,假设类名是A

  1. 将A作为数据成员的类,必须在初始化列表里显示的指定初始化式; 编译器无法为有A类型数据成员的类合成默认构造函数
  2. A类型无法创建动态数组,因为数组元素需要通过默认构造函数初始化
  3. A类型静态数组,必须为每一个元素提供一个显示的初始化式
  4. 包含A类型的容器,如vector,不能使用接受容器大小且没有提供初始化式的构造函数
3.初始化列表

之前我们已经给出定义,构造函数形参右侧,:和左大括号{之间的代码就是初始化列表。在初始化列表里没有提及的数据成员根据变量初始化规则初始化。不管是否有显示的初始化列表,初始化阶段C++都会根据变量初始化规则对数据成员做初始化。

因为内置类型和复合类型并没有在初始化阶段做默认初始化,所以对这些类型的数据成员初始化,通过初始化列表(初始化阶段),或者通过构造函数的函数体(计算阶段),性能上不会有很大差异。

初始化列表中每个数据成员只能出现一次,对同一个数据成员做两次初始化也是无意义的。

4.初始化顺序

数据成员的初始化并不是按照初始化列表顺序进行的,而是按照数据成员定义的顺序。来看个示例,初始化列表定义中先定义b,再用b的值初始化a,在类中定义数据成员的时候,我们先定义a,后定义b

class X {
public:
    int a, b;
    X(int v) : b(v), a(b) {}
};

int main() {
    X x(3);
    std::cout << "a: " << x.a << " ,b: " << x.b << std::endl;
}

程序的输出如下,数据成员a在b之前被初始化,此时b并没有被初始化,值未定义,导致a输出非预期的结果

为了解决这类问题,最好的办法是定义初始化列表的时候,初始化式只依赖形参,而不要依赖其他数据成员。

5.调用构造函数

定义类类型的对象时,如果没指定初始化式,就会调用默认构造函数。比如之前我们定义的Person类,我们可以这样调用默认构造函数

Person p;

调用默认构造函数时,一定不要在变量名后面添加括号(),如果我们添加括号

Person p();

C++会误以为我们在定义一个函数,函数名p,没有形参,返回一个Person类类型的对象。除此以外,可以直接调用构造函数,通过值初始对象p

Person p = Person()

还可以用new关键字创建并返回一个Person对象的指针。

Person *p = new Person()

我的理解是通过new创建的对象是堆上分配的(可能理解有误,欢迎指正),其他的是栈上分配,栈上分配的对象在栈帧弹出的时候自动回收,但堆上分配的对象需要手动回收,要确保这个对象的回收并不是那么轻松的工作。这恐怕也是近年来受欢迎的编程语言多少都是自己垃圾回收机制的原因。

2.隐式转换

C++定义内置类型的自动转换,也允许自定义将其他类型的对象隐式转换为我们的类型,或者将我们的类型隐式转换为其他类型。

拥有单个形参的构造函数,就定义了一个从形参类型到该类类型的隐式转换。

1.定义隐式转换

还是以Person类为例,我们定义了两个只接受一个形参的构造函数,一个接受C风格的字符串字面量;一个接受标准库里的string类型。定义这两个构造函数后,C++能将这两个类型的数据隐式转换为Person对象。

class Person {
public:
    std::string name;
    unsigned age;
    bool male;
    Person(const char *n) : name(n), age(0), male(true)  {}
    Person(const std::string &n) : name(n), age(0), male(true)  {}
};

int main() {
    Person p = "";
    Person p1 = std::string("randy");

    std::cout << " name:'" << p.name << "'" << std::endl;
    std::cout << " name:'" << p1.name << "'" << std::endl;
}

这里有一点比较特殊的是,使用Person p1 = std::string("randy")做隐式转换的时候,构造函数必须定义为Person(const std::string &n),形参必须定义为const。如果期望形参不定义为const,需要先用一个string对象接受字面的值

// 构造函数的定义
Person(std::string &n) : name(n), age(0), male(true)  {}
// 使用隐式转换
std::string s = std::string("randy");
Person p1 = s;

我理解是std::string("randy")返回的是const std::string&类型,通过给一个std::string类型的对象做赋值初始化后,去掉了const约束。

隐式转换不仅仅发生在赋值操作时,任何需要Person类型的位置,传递能隐式转换的类型,都会发生隐式转换

void print_person(const Person& p) {
    std::cout << " name:'" << p.name << "'" << std::endl;
}
// 通过C语言风格的字符串调用
print_person("randy");
2.抑制隐式转换

某些隐式转换并不总是我们期望的行为,这时候可以通过将构造声明为explicit,放在上下文中使用这个函数做隐式转换。 explicit只能用在类内部的构造函数声明上,如果构造函数是类外定义的,定义的时候不需要重复explicit关键字。

explicit Person(const std::string &n) : name(n), age(0), male(true)  {}

定义成explicit后,string类型到Person类型的隐式转换就不再生效了,如果希望把string类型转为Person类型,需要自己显示的调用构造函数。

3.显示初始化

所有的对象都可以通过调用适当的构造函数初始化。对于简单的非抽象类型,所有数据成员都是public,没有定义构造函数,可以采用与初始化数组元素相同的方式初始化数据成员

struct pair
{
    std::string _1;
    std::string _2;
};

int main() {
    pair p = { "a", "b" };
    std::cout << "pair: " << p._1 << p._2 << std::endl;
}

编译器会使用大括号内的数据,按定义顺序,依次初始化每个数据成员。 这种初始化形式是从C语言继承而来,显示初始化有3个重大缺陷

  1. 要求所有数据成员都是public的
  2. 必须每次都要指定所有数据成员
  3. 如果类定义新增或删除了某个数据成员,所有的显示初始化都会出错

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值