本文章是作者根据史蒂芬·普拉达所著的《C++ Primer Plus》而整理出的读书笔记,如果您在浏览过程中发现了什么错误,烦请告知。另外,此书由浅入深,非常适合有C语言基础的人学习,感兴趣的朋友可以自行阅读此书籍。
构造函数
C++的目标之一是让使用类对象就像使用标准类型一样。还是以上一节的类声明为例。
// student.hpp
#ifndef _STUDENT_H_
#define _STUDENT_H_
#include <string>
using namespace std;
class Student{
private:
string id; //为方便处理,将符数组改为string类型
string name; //为方便处理,将字符数组改为string类型
int yuwen;
int shuxue;
int yingyu;
int total;
int avr;
void set_total() { total = yuwen + shuxue + yingyu;}
void set_avr() { avr = total/3;}
public:
void set_info();
void display();
};
#endif
我们无法像初始化int或结构那样初始化Student对象,如下面的方式:
int year = 2024; //正确
struct Animal{
char *name;
int age;
};
Animal am = {"cat", 3}; //正确
Student st = {"00003", "xiaodong", 32, 34, 34, 0, 0}; //错误
有个疑问是,为什么struct可以但是class不行?
那是因为struct的数据成员,默认是public的,因此外部函数可以直接访问,所以可以初始化成功。
而class的数据成员,我们指定了是private的,因此外部函数不可以访问,所以不可以初始化成功。
可以将类改成这种样子(不能这么做,只是为了说明):
// student.hpp
#ifndef _STUDENT_H_
#define _STUDENT_H_
#include <string>
using namespace std;
class Student{
public:
string id;
string name;
int yuwen;
int shuxue;
int yingyu;
int total;
int avr;
void set_total() { total = yuwen + shuxue + yingyu;}
void set_avr() { avr = total/3;}
void set_info();
void display();
};
#endif
这样的话,就可以直接初始化了,但这样使数据成员公有,不满足类的一个主要原则:数据隐藏。,因此我们不能这么做。
那么应该如何初始化呢?
C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员。
构造函数的原型和函数头的特征是,既没有返回值,也没有被声明为void类型。
声明和定义构造函数
我们Student类有7个数据成员,需要初始化的是id、name、yuwen、shuxue和yingyu,total和avr需要计算得到,因此不需要提供此值。
构造函数原型如下:
Student(const string &id, const string &name,
int yuwen = 0, int shuxue = 0, int yingyu = 0);
有个隐藏问题,构造函数中,形参列表表示不是类成员,而是赋给成员的值。因此参数名不能够与类成员相同,否则代码将是这样的:
yuwen = yuwen;
为了避免这种问题,一种常见的做法是在数据成员命中使用m_前缀。
我们把构造函数原型放在类声明中,同时修改数据成员名称:
// student.hpp
#ifndef _STUDENT_H_
#define _STUDENT_H_
#include <string>
using namespace std;
class Student{
private:
string m_id;
string m_name;
int m_yuwen;
int m_shuxue;
int m_yingyu;
int m_total;
int m_avr;
void set_total() { m_total = m_yuwen + m_shuxue + m_yingyu;}
void set_avr() { m_avr = m_total/3;}
public:
Student(const string &id, const string &name,
int yuwen = 0, int shuxue = 0, int yingyu = 0);
void display();
};
#endif
另外,我们再实现构造函数:
//student.cpp
#include <iostream>
#include <string>
#include "student.hpp"
using namespace std;
Student::Student(const string &id, const string &name, int yuwen, int shuxue, int yingyu)
{
m_id = id;
m_name = name;
m_yuwen = yuwen;
m_shuxue = shuxue;
m_yingyu = yingyu;
}
void Student::display()
{
cout << "show info: " << endl;
cout << m_id << " " << m_name << endl
<< "yuwen: " << m_yuwen << endl
<< "shuxue: " << m_shuxue << endl
<< "yingyu: " << m_yingyu<< endl
<< "total : " << m_total << endl
<< "avr : " << m_avr << endl;
}
使用构造函数
C++提供了两种使用构造函数来初始化对象的方式:- 显示地调用构造函数。
- 隐式地调用构造函数。
我们先来看如何显示调用:
Student st1 = Student("00001", "wangdamao", 89, 90, 87);
再看看隐式调用:
Student st2("00002", "xiaoming", 81, 96, 91);
这种方式更加紧凑,并且与显示调用等价。
每次创建类对象时,C++都会使用类构造函数,即使是使用new动态分配内存时。
Student * pst = new Student("00003", "hanmeimei", 83, 92, 95);
这条语句创建一个Student对象,将其初始化为参数提供地值,并将该对象的地址赋给pst指针。在这种情况下,对象没有名称,但可以使用指针来管理该对象。(关于对象指针,我们将在第11章进一步学习)
需要注意的一点是,在构造函数构造出对象之前,对象是不存在的。也就是说,构造函数被用来创建对象,而不能通过对象来调用。
默认构造函数
我们可以定义一个整型变量,但是不给它赋初始值。如:int x;
因此,我们的Student类型,也应该支持这种写法:
Student st;
但是在我们上面的例子中,如果使用Student类时,想要定义一个没有初始值的对象,编译却会报错。
那是因为,如果我们没有提供任何构造函数,则C++将自动提供默认构造函数。它是默认构造函数的隐式版本,不做任何工作。但是如果我们提供了构造函数,那么C++就不会自动提供默认构造函数了。
在上面的Student类中,我们定义了这样的构造函数:
Student::Student(const string &id, const string &name,
int yuwen, int shuxue, int yingyu,
int total = 0, int avr = 0)
那么,我们就只能满足这个构造函数的要求来定义对象。如果我们想要使用Student st的方式。那么我们需要在类中定义默认构造函数。
定义默认构造函数的方式有两种:
- 给已有构造函数的所有参数提供默认值。
- 通过函数重载来定义另一个没有参数的构造函数。
第一种方式如下:
Student::Student(const string &id = "no id", const string &name = "no name",
int yuwen = 0, int shuxue = 0, int yingyu = 0,
int total = 0, int avr = 0)
第二种方式如下,定义一个新的构造函数,给所有的成员提供隐式初始值。
Student::Student()
{
m_id = "no id";
m_name = "no name";
m_yuwen = 0;
m_shuxue = 0;
m_yingyu = 0;
m_total = 0;
m_avr = 0;
}
需要注意的一点是,这两种方式同时只能使用一种,否则会出现二义性的错误。
那么我们应该用哪种方式呢?
在设计类时,通常我们使用第二种方式,也就是通过默认构造函数的方式来对所有类成员做隐式初始化。
在增加默认构造函数之后,我们就可以使用如下的方式来声明对象变量了。
Student st1; //隐式调用默认构造函数
Stduent st2 = Student(); //显示调用默认构造函数
Student *st = new Student; //隐式调用默认构造函数
一个疑点,怎么区分,构造函数的调用方式是显示调用还是隐式调用呢?
最简单的一种分辨方法是,如果看到了函数的完整调用,一般是显示调用,否则是隐式调用。
比如上面第二个语句中使用的Student(),就是我们类声明中的定义的默认构造函数,所以它是显示调用。而第一和第三个语句,并没有直接使用Student(),因此它们为隐式调用。
析构函数 用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数————析构函数。析构函数完成清理工作。
如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除地代码后,提供默认析构函数的定义。
如果构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。如果构造函数没有使用new,那么析构函数实际上没有需要完成的任务。在这种情况下,只需让编译器生成一个什么都不做的隐式析构函数即可。
析构函数的名称比较特殊,需要在类名前加上。因此,Student类的析构函数为Student()。和构造函数一样,析构函数也没有返回值和声明类型。和构造函数不同的是,析构函数没有参数,因此Student析构函数的原型必须是如下的方式:
~Student();
而Student类的析构函数不承担任何重要的工作,因此它的函数实现如下:
Student::~Student()
{
}
什么时候应该调用析构函数呢?
这由编译器决定,通常不应在代码中显示地调用析构函数,一般来说有四种情况:
- 如果创建地是静态存储类对象,如static Student st1,则其析构函数将在程序结束时自动被调用。
- 如果创建的是自动存储类对象,如student st2,则其析构函数将在程序执行完对象所处的代码块时自动被调用。
- 如果对象是通过new 创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,其析构函数将自动被调用。
- 程序可以创建临时对象来完成特定的操作,在这种情况下,程序将在结束对该对象的使用时,自动调用其析构函数。
目前我们已经知道了构造函数、默认构造函数、析构函数的使用方法,现在我们重新把Student类修改下:
// student.hpp
#ifndef _STUDENT_H_
#define _STUDENT_H_
#include <string>
using namespace std;
class Student{
private:
string m_id;
string m_name;
int m_yuwen;
int m_shuxue;
int m_yingyu;
int m_total;
int m_avr;
void set_total() { m_total = m_yuwen + m_shuxue + m_yingyu;}
void set_avr() { m_avr = m_total/3;}
public:
Student();
Student(const string &id, const string &name,
int yuwen = 0, int shuxue = 0, int yingyu = 0);
~Student();
void display();
};
#endif
再修改下类方法的实现:
//student.cpp
#include <iostream>
#include <string>
#include "student.hpp"
using namespace std;
Student::Student()
{
m_id = "no id";
m_name = "no name";
m_yuwen = 0;
m_shuxue = 0;
m_yingyu = 0;
m_total = 0;
m_avr = 0;
cout << "create obj: " << this->m_name << endl;
}
Student::~Student()
{
cout << "delete obj: " << this->m_name << endl;
}
Student::Student(const string &id, const string &name, int yuwen, int shuxue, int yingyu)
{
m_id = id;
m_name = name;
m_yuwen = yuwen;
m_shuxue = shuxue;
m_yingyu = yingyu;
set_total();
set_avr();
cout << "create obj: " << this->m_name << endl;
}
void Student::display()
{
cout << "show info: " << endl;
cout << m_id << " " << m_name << endl
<< "yuwen: " << m_yuwen << endl
<< "shuxue: " << m_shuxue << endl
<< "yingyu: " << m_yingyu<< endl
<< "total : " << m_total << endl
<< "avr : " << m_avr << endl;
}
然后在main函数中,可以如下使用:
//student_main.cpp
#include <iostream>
#include "student.hpp"
using namespace std;
int main()
{
{
static Student st1("00001", "xiaoming");
cout << endl;
Student st2("00002", "Dabai");
cout << endl;
}
cout << endl;
Student st3 = Student("00003", "wangdamao", 89,98,83);
cout << endl;
Student* pst4 = new Student("00004", "Lilei");
cout << endl;
delete pst4;
cout << endl;
Student st5;
cout << endl;
return 0;
}
程序结果如下,为了方便说明情况,加了一些空白行:
create obj: xiaoming
create obj: Dabai
delete obj: Dabai
create obj: wangdamao
create obj: Lilei
delete obj: Lilei
create obj: no name
delete obj: no name
delete obj: wangdamao
delete obj: xiaoming
简单分析下main函数和程序执行结果:
我们在代码块中{}创建了两个对象,其都调用的是带参数的构造函数,一个是静态存储类对象st1(create obj: xiaoming),一个是自动存储类对象st2(create obj: Dabai),我们看到,代码块结束后,st2自动调用了析构函数(delete obj: Dabai)。而st1未被析构。
代码块结束后,我们接着继续创建了自动存储类对象st3(create obj: wangdamao) 和 使用new创建的对象指针pst4(create obj: Lilei),接着我们又使用delete pst4的方式,销毁了这个对象(delete obj: Lilei)。
最后我们创建了一个调用默认构造函数的对象st5,因为不带参数,因此打印(create obj: no name)。
当程序结束后,按照先构造的后析构,后构造的先析构的原则,依次自动析构了st5、st3、st1。