一、类的定义
类内初始值必须以符号=或者花括号表示。
1、成员函数
1)定义成员函数
成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部。定义在类内部的成员函数是隐式的inline函数。
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。因为this的目的总是指向“这个”对象,所以this是一个常量指针,我们不允许改变this中保存的地址。当一个成员调用另外一个成员时,this指针在其中隐式地传递。
2)const成员函数
尽管this是隐式的,但它仍然需要遵循初始化规则,意味着(在默认情况下)我们不能把this绑定到一个常量对象上。这一情况也就使得我们不能在一个常量对象上调用普通的成员函数。
如果this不是隐式的,我们直接把this声明成指向常量的常量指针就可以了。然而,this是隐式的并且不会出现在参数列表中,所以在哪儿将this声明成指向常量的指针就成为我们必须面对的问题。C++语言的做法是允许把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数。常量成员函数不能改变调用它的对象的内容。
1 std::string isbn() const;
常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
3)类作用域和成员函数
类本身就是一个作用域。类的成员函数的定义嵌套在类的作用域之内。
编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数。因此,成员函数体,可以随意使用类中的其他成员而无须在意这些成员出现的次序。
4)在类的外部定义成员函数
当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。也就是说,返回类型、参数列表和函数名都得与类内部的声明保持一致。如果成员被声明为常量成员函数,那么它的定义也必须在参数列表后明确指定const属性。
1 std::string SalesData::isbn() const 2 { 3 return this->m_book_no; 4 }
5)返回this对象的函数
我们无需使用隐式的this指针访问函数调用者的某个具体成员,而是需要把调用函数的对象当成一个整体来访问:
1 SalesData &SalesData::combine(const SalesData &rhs) 2 { 3 m_units_sold += rhs.m_units_sold; 4 m_revenue += rhs.m_revenue; 5 return *this; // 返回调用该函数的对象 6 }
一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。
2、定义类相关的非成员函数
类的作者常常需要定义一些辅助函数,比如add、read、print等。尽管这些函数定义的操作从概念上来说属于类的接口组成部分,但它们实际上并不属于类本身。
一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类声明在同一个头文件内。在这种方式下,用户使用接口的任何部分都只需要引入一个文件。
3、构造函数
构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数不能被声明成const的。当我们创建一个类的const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
1)合成的默认构造函数
如果我们没有为类的对象提供初始值,那么类就会执行默认初始化。类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。默认构造函数无须任何实参。
如果类没有显示地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数。编译器创建的构造函数又被称为合成的默认构造函数。对于大多数类来说,这个合成的默认构造函数将按照如下规则初始化类的数据成员:如果存在类内初始值,用它来初始化成员;否则,默认初始化该成员。
2)某些类不能依赖于合成的默认构造函数
a、编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。
b、对于某些类来说,合成的默认构造函数可能执行错误的操作。如果定义在块中的内置类型或复合类型(比如数组和指针)的对象被默认初始化,则它们的值将是未定义的。该准则同样适用于默认初始化的内置类型成员。因此,含有内置类型或复合类型成员的类应该在类的内部初始化这些成员,或者定义一个自己的默认构造函数。否则,用户在创建类的对象时就可能得到未定义的值。如果类包含有内置类型或复合类型的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。
c、有的时候编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。对于这样的类来说,我们必须自定义默认构造函数,否则该类将没有可用的默认构造函数。
3)=default的含义
在C++11标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上 = default 来要求编译器生成构造函数。其中,= default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果= default在类的内部,则默认构造函数是内联的;如果它在类的外部,则该成员默认情况下不是内联的。
如果你的编译器不支持类内初始值,那么你的默认构造函数应该使用构造函数初始值列表来初始化类的每个成员。
4)构造函数初始值列表
1 SalesData(const std::string &s) :m_book_no(s){} 2 SalesData(const std::string &s, unsigned n, double p):
冒号与花括号之间的部分就是构造函数初始值列表,它负责为新创建的对象的一个或几个数据成员赋初始值。构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的成员初始值,不同成员的初始化通过逗号分开来。没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在的话)初始化,或者执行默认初始化。
如果你的编译器不支持类内初始值,则所有构造函数都应该显示地初始化每个内置类型的成员。
4、拷贝、赋值和析构
如果我们不主动定义这些操作,则编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。
尽管编译器能替我们合成拷贝、赋值和销毁的操作,但是必须要清楚的一点是,对于某些类来说合成的版本无法正常工作。特别是,当类需要分配类对象之外的资源时,合成的版本常常会失效。
二、访问控制与封装
1、访问说明符
定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。
一个类可以包含0个或多个访问说明符,而且对于某个访问说明符能出现多少次也没有严格限定。每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者到达类的结尾处为止。
2、使用class或struct关键字
使用class或struct定义类的唯一区别就是默认的访问权限。
类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式。如果我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的;相反,如果我们使用class关键字,则这些成员是private的。
3、友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。

1 class SalesData 2 { 3 // 友元声明 4 friend SalesData add(const SalesData &, const SalesData &); 5 friend std::ostream &print(std::ostream &, const SalesData &); 6 friend std::istream &read(std::istream &, SalesData &); 7 public: 8 SalesData() = default; 9 SalesData(const std::string &s) :m_book_no(s){} 10 SalesData(const std::string &s, unsigned n, double p): 11 m_book_no(s), m_units_sold(n), m_revenue(p * n){} 12 13 std::string isbn() const; 14 SalesData &combine(const SalesData &); 15 private: 16 double avgPrice() const; 17 18 std::string m_book_no; // 书名 19 unsigned m_units_sold = 0; // 数量 20 double m_revenue = 0.0; // 总价 21 }; 22 23 // SalesData的非成员接口函数的声明 24 SalesData add(const SalesData &, const SalesData &); 25 std::ostream &print(std::ostream &, const SalesData &); 26 std::istream &read(std::istream &, SalesData &);
友元声明只能出现在类的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域的访问控制级别的约束。
友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。
为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中(类的外部)。
一个完整的列子:
SalesData.h

1 #ifndef SALES_DATA 2 #define SALES_DATA 3 4 #include <iostream> 5 #include <string> 6 class SalesData 7 { 8 // 友元声明 9 friend SalesData add(const SalesData &, const SalesData &); 10 friend std::ostream &print(std::ostream &, const SalesData &); 11 friend std::istream &read(std::istream &, SalesData &); 12 public: 13 SalesData() = default; 14 SalesData(const std::string &s) :m_book_no(s){} 15 SalesData(const std::string &s, unsigned n, double p): 16 m_book_no(s), m_units_sold(n), m_revenue(p * n){} 17 18 std::string isbn() const; 19 SalesData &combine(const SalesData &); 20 private: 21 double avgPrice() const; 22 23 std::string m_book_no; // 书名 24 unsigned m_units_sold = 0; // 数量 25 double m_revenue = 0.0; // 总价 26 }; 27 28 // SalesData的非成员接口函数的声明 29 SalesData add(const SalesData &, const SalesData &); 30 std::ostream &print(std::ostream &, const SalesData &); 31 std::istream &read(std::istream &, SalesData &); 32 33 #endif
SalesData.cpp

1 #include "SalesData.h" 2 #include <string> 3 4 std::string SalesData::isbn() const 5 { 6 return this->m_book_no; 7 } 8 9 SalesData &SalesData::combine(const SalesData &rhs) 10 { 11 m_units_sold += rhs.m_units_sold; 12 m_revenue += rhs.m_revenue; 13 return *this; // 返回调用该函数的对象 14 } 15 16 double SalesData::avgPrice() const 17 { 18 if (m_units_sold) 19 return m_revenue / m_units_sold; 20 else 21 return 0; 22 } 23 24 SalesData add(const SalesData &lhs, const SalesData &rhs) 25 { 26 SalesData sum = lhs; 27 sum.combine(rhs); 28 return sum; 29 } 30 31 std::ostream &print(std::ostream &os, const SalesData &item) 32 { 33 os << item.isbn() << " " << item.m_units_sold << " " 34 << item.m_revenue << " " << item.avgPrice(); 35 return os; 36 } 37 38 std::istream &read(std::istream &is, SalesData &item) 39 { 40 double price = 0; 41 is >> item.m_book_no >> item.m_units_sold >> price; 42 item.m_revenue = price * item.m_units_sold; 43 return is; 44 }
main.cpp

1 #include <iostream> 2 #include <string> 3 #include "../header/SalesData.h" 4 5 int main() 6 { 7 SalesData s1("book", 10,5.5), s2("book",10,5.5), sum; 8 sum = add(s1, s2); 9 print(std::cout, sum); 10 std::cout << std::endl; 11 return 0; 12 }