# 构造函数 构造函数是一种特殊的类成员函数,在创建类对象时被调用。构造函数的名称和类型相同,通过函数重载可以创建多个同名的构造函数,每个函数的参数列表应不同。构造函数没有声明类型,构造函数用于初始化类对象的成员,初始化应与构造函数的参数列表匹配。一般在头文件中对类成员数据和成员函数声明,在源文件中对成员函数定义实现。 这种特殊的成员函数,它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数(Constructor)。 有了构造函数,我们就可以简化这项工作,在创建对象的同时为成员变量赋值,请看下面的代码: ```c++ #include using namespace std; class Student { public: //声明构造函数 Student(const char *name, int age, float score); void setname(const char *name); void setage(int age); void setscore(float score); void show(); void printThis(); private: const char *name; int age; float score; }; //定义构造函数 Student::Student(const char *name, int age, float score) { this->name = name; this->age = age; this->score = score; } //普通成员函数 void Student::setname(const char *name) { this->name = name; } void Student::setage(int age) { this->age = age; } void Student::setscore(float score) { this->score = score; } void Student::show() { cout << this->name << "的年龄是" << this->age << ",成绩是" << this->score << endl; } void Student::printThis() { cout << this << endl; } int main() { //创建对象时向构造函数传参 Student stu("小明", 15, 90.1f); stu.show(); //创建构造函数时向构造函数传参 Student *pstu = new Student("wangyueodng", 18, 88.8f); pstu->show(); return 0; } ``` 该例在 Student 类中定义了一个构造函数`Student(char *, int, float)`,它的作用是给三个 private 属性的成员变量赋值。要想调用该构造函数,就得在创建对象的同时传递实参,并且实参由`( )`包围,和普通的函数调用非常类似。在栈上创建对象时,实参位于对象名后面,例如`Student stu("小明", 15, 92.5f)`;在堆上创建对象时,实参位于类名后面,例如`new Student("李华", 16, 96)`。 ## 构造函数需要注意的点 构造函数必须是 public 属性的,否则创建对象时无法调用。当然,设置为 private、protected 属性也不会报错,但是没有意义。 构造函数没有返回值,因为没有变量来接收返回值,即使有也毫无用处,这意味着: - 不管是声明还是定义,函数名前面都不能出现返回值类型,即使是 void 也不允许; - 函数体中不能有 return 语句。 ## 构造函数的重载 和普通成员函数一样,构造函数是允许重载的。一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。 构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。 对示例1中的代码,如果写作`Student stu`或者`new Student`就是错误的,因为类中包含了构造函数,而创建对象时却没有调用。 ```c++ #include using namespace std; class Student { public: //声明构造函数 Student(); Student(const char *name, int age, float score); void setname(const char *name); void setage(int age); void setscore(float score); void show(); void printThis(); private: const char *name; int age; float score; }; //定义构造函数 Student::Student() { this->name = NULL; this->age = 0; this->score = 0.0f; } Student::Student(const char *name, int age, float score) { this->name = name; this->age = age; this->score = score; } //普通成员函数 void Student::setname(const char *name) { this->name = name; } void Student::setage(int age) { this->age = age; } void Student::setscore(float score) { this->score = score; } void Student::show() { cout << this->name << "的年龄是" << this->age << ",成绩是" << this->score << endl; } void Student::printThis() { cout << this << endl; } ``` ## 默认构造函数 如果用户自己没有定义构造函数,那么编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空的,也没有形参,也不执行任何操作。比如上面的 Student 类,默认生成的构造函数如下: ```c++ Student() {} ``` 一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。在示例1中,Student 类已经有了一个构造函数`Student(char *, int, float)`,也就是我们自己定义的,编译器不会再额外添加构造函数`Student()`,在示例2中我们才手动添加了该构造函数。 > 实际上编译器只有在必要的时候才会生成默认构造函数,而且它的函数体一般不为空。默认构造函数的目的是帮助编译器做初始化工作,而不是帮助程序员。这是C++的内部实现机制,这里不再深究,初学者可以按照上面说的“一定有一个空函数体的默认构造函数”来理解。 最后需要注意的一点是,调用没有参数的构造函数也可以省略括号。对于示例2的代码,在栈上创建对象可以写作`Student stu()`或`Student stu`,在堆上创建对象可以写作`Student *pstu = new Student()`或`Student *pstu = new Student`,它们都会调用构造函数 Student()。 以前我们就是这样做的,创建对象时都没有写括号,其实是调用了默认的构造函数。 ## 构造函数初始化列表 构造函数的一项重要功能是对成员变量进行初始化,为了达到这个目的,可以在构造函数的函数体中对成员变量一一赋值,还可以采用初始化列表。 ```c++ #include using namespace std; class Student { private: const char *m_name; int m_age; float m_score; public: Student(const char *name, int age, float score); void show(); }; //采用初始化列表 Student::Student(const char *name, int age, float score) : m_name(name), m_age(age), m_score(score) { //TODO } void Student::show() { cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl; } int main() { Student stu("小明", 18, 92.5f); stu.show(); Student *pstu = new Student("小王", 28, 98.8f); pstu->show(); return 0; } ``` 定义构造函数时并没有在函数体中对成员变量一一赋值,其函数体为空(当然也可以有其他语句),而是在函数首部与函数体之间添加了一个冒号`:`,后面紧跟`m_name(name), m_age(age), m_score(score)`语句,这个语句的意思相当于函数体内部的`m_name = name; m_age = age; m_score = score;`语句,也是赋值的意思。 使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简单明了。 初始化列表可以用于全部成员变量,也可以只用于部分成员变量。下面的示例只对 m_name 使用初始化列表,其他成员变量还是一一赋值: ```c++ Student::Student(char *name, int age, float score): m_name(name){ m_age = age; m_score = score; } ``` 注意,成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。请看代码: ## 初始化const成员变量 构造函数初始化列表还有一个很重要的作用,那就是初始化 const 成员变量。初始化 const 成员变量的唯一方法就是使用初始化列表。例如 VS/VC 不支持变长数组(数组长度不能是变量),我们自己定义了一个 VLA 类,用于模拟变长数组,请看下面的代码: ```c++ class VLA{ private: const int m_len; int *m_arr; public: VLA(int len); }; //必须使用初始化列表来初始化 m_len VLA::VLA(int len):m_len(len){ m_arr = new int[len]; } ``` VLA 类包含了两个成员变量,m_len 和 m_arr 指针,需要注意的是 m_len 加了 const 修饰,只能使用初始化列表的方式赋值,如果写作下面的形式是错误的: ```c++ class VLA{ private: const int m_len; int *m_arr; public: VLA(int len); }; VLA::VLA(int len){ m_len = len; m_arr = new int[len]; } ``` 有时MSVS编译器这样写也可以编译运行成功,但是还是需要注意一下。