在大多数C++程序中,类都是至关重要的:我们能够使用类来定义为要解决的问题定制的数据类型,从而得到更加易于编写和理解的应用程序。设计良好的类类型可以像内置类型一样容易使用。
类的定义和声明
类的定义格式如下:
class ClassName {
members; // 类的成员,可以是数据成员或成员函数
}objectNames; // objectNames是可选的
注意:类定义后的分号是必不可少的。
在理解类的定义之前,先记住下面几个术语:
类成员:类可以定义多个成员,成员可以是数据、函数或类型别名。类也可以没有成员,但没有太大的实际意义。所有成员必须在类的内部声明,即在定义类时的{}之内声明,一旦类定义完成后,就没有任何方式可以增加成员了。
构造函数:构造函数是一个特殊的、与类名同名的成员函数,用于给每个数据成员设置适当的初始值。只要创建类类型的新对象,都要执行构造函数,因此构造函数一定要声明为public成员。
访问标号:决定类成员的访问权限,可以使用的关键字是public、private、protected。
public:位于public之后的类成员,程序中的所有部分都可以访问。
private:位于private之后的类成员,只能在类的内部访问。
protected:位于protected之后的类成员,同private,但是可被该类的派生类对象访问。
通过下面这个简单的例子,加深对构造函数的理解:
class MyFirstClass {
public: // 访问标号public是必不可少的
MyFirstClass(){
cout << "This is my first class!" << endl;
}
};
int main()
{
MyFirstClass myObj; // 调用构造函数MyFirstClass(){}
return 0;
}
运行结果:
This is my first class!
测试:去掉public标号,编译程序,看看会发生什么?
构造函数的名字与类的名字相同,并且不能指定返回类型,像其它任何函数一样,可以没有形参,也可以定义多个形参,马上我们将会看到定义形参的用法。没有形参的构造函数(或者是为所有形参提供默认实参的构造函数)我们称之为默认构造函数。只要在创建一个对象时没有提供初始化式,类就会调用默认构造函数。
上面的MyFirstClass类没有定义数据成员,也没有实际的意义,仅仅是为了演示构造函数的调用。下面我们定义一个稍微复杂一点的类,该类包含数据成员和一些构造函数。为了更好的和现实联系起来,我们把人抽象成一个类Person。人有姓名和年龄,所以我们把name和age作为Person类的两个数据成员,并定义一个成员函数print打印name和age:
class Person {
public:
Person(){ // 默认构造函数
name = "";
age = 0;
}
Person(string N, int A){ // 带有形参的构造函数
name = N;
age = A;
}
void print(){
cout << "My name is " << name << ",and I'm " << age << " years old." << endl;
}
private: // 一般把数据成员定义成私有成员
string name;
int age;
};
构造函数也和普通的函数一样可以被重载,只要每个构造函数的形参表是唯一的。Person类中定义了两个构造函数,带形参的和不带形参的。如果我们想创建一个名字叫Tyrone,20岁的人,可以用下面的代码实现:
Person me("Tyrone",20); // 自动调用Person(string N, int A)构造函数
me.print();// 对象通过点操作符访问public成员,不能访问private成员
运行结果:
My name is Tyrone,and I’m 20 years old.
如果我们在创建对象me的时候没有指定实参,会调用默认构造函数:
Person me; // 注意对象名me后没有(),自动调用默认构造函数Person()
me.print();
运行结果:
My name is ,and I’m 0 years old.
采用下面的方式创建对象是错误的:
Person me();
编译器会把me()解释为一个函数的声明,这个函数不接受参数并返回一个Person类型的对象。使用默认构造函数定义一个对象的正确方式是去掉最后的空括号,或者用下面的方式:
Person me = Person();
试想,如果我们没有定义任何构造函数会发生什么呢?编译器会自动生成一个默认构造函数,称作合成的默认构造函数。只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。一个类哪怕只定义了一个构造函数,编译器就不会生成默认构造函数。
合成的默认构造函数使用与变量初始化相同的规则来初始化成员。具有类类型的成员通过运行各自的默认构造函数来进行初始化。内置和复合类型的成员,如指针和数组,只对定义在全局作用域的对象才初始化。当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化。
下面我们把Person类的构造函数去掉:
class Person {
public:
void print(){
cout << "My name is " << name << ",and I'm " << age << " years old." << endl;
}
private:
string name;
int age;
};
自己动手运行下面两段代码,观察运行结果,看看有什么不同,并思考为什么?
代码一:
Person me;
int main()
{
me.print();
return 0;
}
代码二:
int main()
{
Person me;
me.print();
return 0;
}
简化构造函数:使用构造函数初始化列表
现在我们应该已经知道,构造函数的工作就是保证每个对象的数据成员具有合适的初始值。构造函数和普通函数的区别就是没有返回类型,另外,还有一个区别是,构造函数可以包含一个初始化列表。
构造函数初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。
值的注意的是,构造函数初始化式只在构造函数的定义中而不是声明中指定。
在结合函数默认实参的性质,可以简化构造函数,下面我们重新定义Person类:
class Person {
public:
Person(string N = "", int A = 0):name(N),age(A){};
void print(){
cout << "My name is " << name << ",and I'm " << age << " years old." << endl;
}
private:
string name;
int age;
};
前面我们说过,为所有形参都提供默认实参的构造函数也是默认构造函数,所以下面两种创建对象的方式都是正确的:
Person me; // 默认实参
Person me("Tyrone", 20);
构造函数初始化式不仅仅是为了简化构造函数,有些成员必须在构造函数初始化列表中进行初始化,对于这样的成员,在构造函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及const或引用类型的成员,都必须在构造函数初始化列表中进行初始化。以const成员为例,在Person类中,我们增加一个const成员性别sex:
const string sex;
那么就要修改构造函数为:
Person(string N = "", int A = 0, const string S = "female"):name(N),age(A), sex(S){};
在构造函数体内对其赋值是错误的:
Person(string N = "", int A = 0, const string S = "female"):name(N),age(A){
sex = S;
};
构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序。成员被初始化的次序就是定义成员的次序。