在 C++ 中,“构造函数(Constructor)” 是一种特殊的成员函数。每当我们根据类创建对象时,编译器都会自动调用构造函数,用来完成成员变量的初始化工作。
前面操作类与对象时,我们每次写完 Student stu; 创建对象后,往往还要再写好几行代码给成员变量逐个赋值。有了构造函数,我们在创建对象的同时就能把初始值设置好,也不用担心漏写赋值语句而读出随机的垃圾值。
C++ 构造函数的基本用法
在 C++ 中,构造函数同样定义在类的内部,但它的声明写法和普通成员函数有明显区别。
语法:
class 类名 {
public:
类名(参数列表) {
// 初始化代码
}
};说明:
编写构造函数时,我们需要遵循以下 3 条语法规则:
- 函数名与类名完全相同:构造函数的名字必须和类名一模一样,包括字母的大小写。
- 没有返回值类型:构造函数前面不能写任何返回值类型,注意连
void都不能写。一旦写了void,编译器就会把它当成普通成员函数,而不再是构造函数。 - 创建对象时自动调用:普通成员函数需要通过
对象名.函数名()主动调用,而构造函数是在创建对象时由编译器自动执行的。
需要注意的是,类的访问修饰符默认为 private,因此构造函数通常要放在 public: 下面,这样我们才能在类的外部正常创建对象。如果一个构造函数不需要接收任何参数,我们就把它称为 “无参构造函数”,平时也常叫做 “默认构造函数”。下面我们来看一个例子。
示例 1:定义无参构造函数
#include <iostream>
#include <string>
class Book {
private:
std::string title;
int price;
public:
// 无参构造函数
Book() {
title = "未命名图书";
price = 0;
std::cout << "构造函数已自动执行\n";
}
void showInfo() {
std::cout << "《" << title << "》" << price << " 元\n";
}
};
int main() {
Book book; // 自动调用 Book()
book.showInfo();
return 0;
}运行结果如下。
构造函数已自动执行
《未命名图书》0 元分析:
在这个例子中,我们在 Book 类内部定义了一个无参构造函数 Book()。当我们在 main() 函数中执行 Book book; 创建对象时,虽然没有写任何手动调用的代码,但在 book 创建出来的那一刻,编译器就已经自动执行了 Book()。
因此,屏幕上先打印出了 “构造函数已自动执行”,同时 book 内部的 title 和 price 也被赋值为 "未命名图书" 和 0。
注意 调用无参构造函数创建对象时,我们直接写 Book book; 或 Book book{}; 即可,千万不要加空括号写成 Book book();。因为 Book book(); 会被编译器当成普通的函数声明,而不是在创建对象。
带参构造函数与构造函数重载
上面的无参构造函数虽然省事,但创建出来的每一本图书,初始书名和价格都是固定不变的。如果我们希望在创建对象的同时,直接把具体的书名和价格传进去,就可以给构造函数加上参数。
示例 2:使用带参构造函数
#include <iostream>
#include <string>
class Book {
private:
std::string title;
int price;
public:
// 带参构造函数
Book(std::string bookTitle, int bookPrice) {
title = bookTitle;
price = bookPrice;
}
void showInfo() {
std::cout << "《" << title << "》" << price << " 元\n";
}
};
int main() {
Book book("C++ 教程", 68); // 创建对象时传入初始值
book.showInfo();
return 0;
}运行结果如下。
《C++ 教程》68 元分析:
Book book("C++ 教程", 68); 这句代码表示在创建 book 对象的同时,直接把小括号里的两个实参传给带参构造函数。其中,"C++ 教程" 传给形参 bookTitle,68 传给形参 bookPrice,并在函数体内赋值给当前对象的成员变量。
在实际开发中,创建对象的方式往往不止一种。有时我们还不知道具体信息,有时只确定了书名,有时则已经确定了完整的书名和价格。就像普通函数支持函数重载一样,同一个类中也可以同时定义多个构造函数,只要它们的参数个数或参数类型不同即可。请看下面例子。
示例 3:重载多个构造函数
#include <iostream>
#include <string>
class Book {
private:
std::string title;
int price;
public:
// 1. 无参构造函数
Book() {
title = "未命名图书";
price = 0;
}
// 2. 单参构造函数
Book(std::string bookTitle) {
title = bookTitle;
price = 0;
}
// 3. 双参构造函数
Book(std::string bookTitle, int bookPrice) {
title = bookTitle;
price = bookPrice;
}
void showInfo() {
std::cout << "《" << title << "》" << price << " 元\n";
}
};
int main() {
Book book1; // 匹配无参构造函数
Book book2("C++ 教程"); // 匹配单参构造函数
Book book3("SQL 快速上手", 59); // 匹配双参构造函数
book1.showInfo();
book2.showInfo();
book3.showInfo();
return 0;
}运行结果如下。
《未命名图书》0 元
《C++ 教程》0 元
《SQL 快速上手》59 元分析:
在这个例子中,Book 类重载了 3 个参数不同的构造函数,创建对象时编译器会根据实参自动匹配。其中,book1 不传参,匹配第 1 个无参构造函数。book2 只传入了书名,匹配第 2 个单参构造函数。book3 同时传入了书名和价格,匹配第 3 个双参构造函数。
默认构造函数的生成规则与 = default
可能有些小伙伴会问:“前两节我们在定义 Student 类时明明没有写任何构造函数,为什么依然能写出 Student stu; 正常创建对象呢?” 这就要说到编译器自动生成默认构造函数的 2 条规则了:
- 不写任何构造函数时,编译器会自动生成默认构造函数:如果一个类中没有手写任何构造函数,C++ 编译器会自动为它生成一个空的无参默认构造函数,保证我们能正常创建对象。
- 一旦手写了构造函数,编译器就不再自动生成无参构造函数:只要我们在类中手写了任何一个构造函数(哪怕只写了一个带参构造函数),编译器就会认为我们已经自己接管了初始化工作。此时,它不会再自动提供默认的无参构造函数。
提示 当类中没有手写构造函数,也没有给成员变量指定类内默认初始值时,编译器自动生成的默认构造函数并不会把 int、double 这类基本类型成员清零,它们的值依然是不确定的随机垃圾值。
很多初学的小伙伴最容易在第 2 条规则上踩坑:在类中只定义了一个带参构造函数,随后又去写不带参数的创建语句,结果直接引发编译报错。下面我们通过一个例子来看看。
示例 4:缺少无参构造函数报错
#include <iostream>
class Book {
private:
int price;
public:
Book(int bookPrice) {
price = bookPrice;
}
};
int main() {
Book book1(68); // 正常匹配带参构造
Book book2; // 错误:没有无参构造函数
return 0;
}运行结果如下。
(报错)error: no matching function for call to 'Book::Book()' 分析:
因为我们在 Book 类中已经手写了带参构造函数 Book(int bookPrice),编译器不再自动生成无参构造函数。此时执行 Book book2; 时,编译器找不到无参的 Book(),就会直接报错,提示没有匹配的函数(no matching function)。
如果类中已经定义了带参构造函数,同时我们又希望保留无参创建对象的方式,应该怎么办呢?除了像示例 3 那样自己手写一个无参构造函数之外,从 C++11 起,我们还可以在无参构造函数声明末尾加上 = default;,要求编译器帮我们生成默认无参构造函数。
示例 5:使用 = default
#include <iostream>
#include <string>
class Book {
private:
std::string title = "未命名图书"; // 类内默认初始值
int price = 0; // 类内默认初始值
public:
Book() = default; // 生成默认无参构造
Book(std::string bookTitle, int bookPrice) {
title = bookTitle;
price = bookPrice;
}
void showInfo() {
std::cout << "《" << title << "》" << price << " 元\n";
}
};
int main() {
Book book1; // 调用默认无参构造
Book book2("Python 教程", 79); // 调用带参构造
book1.showInfo();
book2.showInfo();
return 0;
}运行结果如下。
《未命名图书》0 元
《Python 教程》79 元分析:
Book() = default; 这句代码表示要求编译器重新生成默认的无参构造函数,这样 Book book1; 就能正常编译通过了。
这里大家要留意一个细节:Book() = default; 本身并不会自动把 int 这类基本类型成员初始化为 0。例子中 book1 的价格之所以能安全输出 0,是因为我们在定义成员变量时直接写上了 int price = 0; 指定了类内默认初始值。
当成员变量已经指定了类内默认初始值,需要保留一个不做额外操作的无参构造函数时,下面 2 种方式是等价的。
// 方式 1(推荐)
Book() = default;
// 方式 2
Book() {}在实际开发中,我们更推荐使用方式 1,配合类内默认初始值一起使用,比手写一对空花括号更简洁直观。
