类1(this指针/const成员函数/类作用域/外部成员函数/返回this对象的函数)
假设我们要设计一个包含以下操作的 Sales_data 类:
1.一个 isbn 成员函数,用于返回对象的 book_no 成员变量
2.一个 combine 成员函数,用于将一个 Sales_data 对象加到另一个 Sales_data 对象上
3.一个名为 add 的函数,执行两个 Sales_data 对象的加法
4.一个 read 函数,将数据从 istream 都入到 Sales_data 对象中
5.一个 print 函数,将 Sales_data 对象的值输出到 ostream
1 struct Sales_data{ 2 //数据成员 3 std::string book_no; 4 unsigned units_sold = 0; 5 double revenue = 0.0; 6 7 //函数成员 8 std::string isbn() const { 9 return book_no; 10 // return this->book_no;//等价语句 11 } 12 Sales_data& combine(const Sales_data&); 13 double avg_price() const; 14 }; 15 //Sales_data的非成员函数声明 16 Sales_data add(const Sales_data&, const Sales_data&); 17 std::ostream &print(std::ostream&, const Sales_data&); 18 std::istream &read(std::istream&, Sales_data&);
注意到:
类的成员函数的声明必须在类的内部,它的定义则既可以在类的内部,也可以在内的外部。
作为接口组成部分的非成员函数,它的定义和声明都在类的外部。
定义在类内部的函数都是隐式的 inline 函数。
引入 this:
在上列中,isbn 函数中只有一条 return 语句,用于返回 Sales_data 对象的 book_no 数据成员。关于 isbn 函数一件有意思的事情是:它是如何获得 bool_no 成员所依赖的对象呢?
Sales_data total;//创建一个Sales_data类的实例
total.isbn();//访问 total 对象的 isbn 成员函数
可以发现,当我们调用某个类成员函数的时候,实际上是在替某个对象调用它。如果 isbn 指向 Sales_data 的成员,则它隐式的指向调用该函数的对象的成员。如上列所示的调用中,当 isbn 返回 book_no 时,实际上它隐式的返回 total.book_no。
成员函数通过一个名为 this 的额外的隐式参数来访问调用它的那个对象。this 是一个指向对象本身的常量指针。所以 return book_no;等价于 return this->book_no;其效果是 return total.bool_no。
引入 const 成员函数:
isbn 函数的另一个关键之处是紧随参数列表之后的 const 关键字,这里,const 的作用是修改隐式 this 指针的类型。默认情况下 this 的类型是指向类类型非常量版本的常量指针(具有顶层 const 但不具有底层 const)。例如在 Sales_data 成员函数中,this 的类型是 Sales_data *const。尽管 this 是隐式的,但它仍然需要遵循初始化规则,意味着在默认情况下我们不能把 this 绑定到一个常量对象上。这一情况使得我们不能在一个常量对象上调用普通的成员函数:
1 #include <iostream> 2 using namespace std; 3 4 struct Sales_data{ 5 //数据成员 6 std::string book_no; 7 //函数成员 8 std::string isbn() { 9 return book_no; 10 // return this->book_no;//等价语句 11 } 12 }; 13 14 int main(void){ 15 Sales_data total; 16 const Sales_data gel; 17 total.isbn(); 18 // gel.isbn();//错误: isbn 成员函数没有将隐式的 this 指针声类型修改成具有底层 const,所以 isbn 成员函数不能被常量对象调用 19 return 0; 20 }
常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
类作用域和成员函数:
类本身就是一个作用域。类的成员函数的定义嵌套在类的作用域之内,因此,isbn 中用到的名字 book_no 其实就是定义在 Sales_data 内的数据成员。值得注意的是,即使 book_no 定义在 sibn 之后,isbn 也还是能够使用 book_no。编译器分两部处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)。因此,成员函数体可以随意使用类中的其他成员而无需在意这些成员出现的次序:
1 struct Sales_data{ 2 //函数成员 3 std::string isbn() { 4 return book_no; 5 // return this->book_no;//等价语句 6 } 7 //数据成员 8 std::string book_no; 9 };
需要特别注意的是,由于编译器要处理完类中的全部声明后才会处理成员函数的定义,成员函数中使用的名字我们可以不用在意它声明在成员函数定义之前还是之后。但是声明中使用的名字(定义的类型名等),包括返回类型或者参数列表中使用的名字,都必须在使用前可见。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找:
1 #include <iostream> 2 using namespace std; 3 4 using small_int = string; 5 6 class ac26{ 7 public: 8 void lou(small_int cnt){//这里的small_int等价于string而非int 9 gel += 1; 10 } 11 12 private: 13 int gel = 0; 14 using small_int = int; 15 }; 16 17 int main(void){ 18 ac26 x; 19 x.lou(3); 20 return 0; 21 }
当编译器看到 lou 函数的声明语句时,它先会在类 ac26 中 lou 声明之前的范围内查找 small_int 的声明。因为没有找到匹配的成员,所以编译器会接着到 ac26 的外层作用域中查找。在本例中,编译器会找到 using small_int = string;语句,所以 lou 的形参 cnt 是 string 类型的。在 main 函数中 x.lou(3) 调用会 error。
还有一点需要注意的是:对于成员函数内部使用的名字,如果在类内没有找到声明,编译器会接着在外层作用域接着查找(这一点是和用于类成员声明的名字一样的):
1 #include <iostream> 2 using namespace std; 3 4 string book_no = "world"; 5 6 class Sales_data1{ 7 public: 8 std::string isbn() { 9 return book_no;//返回的是Sales_data1类内定义的book_no变量 10 } 11 12 private: 13 std::string book_no = "hello"; 14 }; 15 16 class Sales_data2{ 17 public: 18 std::string isbn() { 19 return book_no;//返回Sales_data2类外定义的book_no 20 } 21 22 // private: 23 // std::string book_no = "hello"; 24 }; 25 26 int main(void){ 27 Sales_data1 x1; 28 Sales_data2 x2; 29 cout << x1.isbn() << endl;//输出hello 30 cout << x2.isbn() << endl;//输出world 31 return 0; 32 }
在类外部定义成员函数:
像其他函数一样,当我们在类的外面定义成员函数时,成员函数的定义必须与它的声明匹配。也就是说,返回类型,参数列表和函数名都得与类内部的声明保持一致。如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定 const 属性。同时,类外部定义的成员的名字必须包含它所属的类名:
1 #include <iostream> 2 using namespace std; 3 4 struct Sales_data{ 5 //数据成员 6 unsigned units_sold = 0; 7 double revenue = 0.0; 8 9 //成员函数声明 10 double avg_price() const; 11 }; 12 13 double Sales_data::avg_price() const{ 14 if(units_sold) return revenue / units_sold;//定义在类外部的成员函数也能直接使用类成员 15 return 0; 16 } 17 18 int main(void){ 19 Sales_data total; 20 cout << total.avg_price() << endl; 21 return 0; 22 }
定义一个返回 this 对象的函数:
在一开始的那个 Sales_data 类的设计代码中,函数 combine 的设计初衷类似于复合赋值运算符 +=,调用该函数的对象代表的时赋值运算符左侧的运算对象,右侧运算对象则通过显示的实参被传入函数:
1 #include <iostream> 2 using namespace std; 3 4 struct Sales_data{ 5 //数据成员 6 std::string book_no; 7 unsigned units_sold = 0; 8 double revenue = 0.0; 9 10 //成员函数声明 11 Sales_data& combine(const Sales_data&); 12 }; 13 14 Sales_data& Sales_data::combine(const Sales_data &rhs){ 15 units_sold += rhs.units_sold; 16 revenue += rhs.revenue; 17 return *this;//解引用获得调用该函数的对象本身 18 } 19 20 int main(void){ 21 Sales_data total, x; 22 total.combine(x);//计算 total + x 并将结果保持到 total 23 //total 的地址被绑定到隐式的 this 参数上,而引用 rhs 绑定到了 x 上 24 return 0; 25 }
其中,return 语句解引用 this 以获得执行该函数的对象,即上面的这个调用返回 total 的引用。
定义类相关的非成员函数:
像前面的 add, read, print 函数,尽管这些函数定义的操作从概念上来说属于类的接口组成部分,但它们实际上不属于类本身。我们应该将其定义成非成员函数。定义非成员函数和其他函数一样,通常把函数的声明和定义分离开来。如果函数概念上属于类但是不定义在类中,则它一般应与类声明在同一个头文件内。在这种方式下,用户使用接口的任何部分都只需要引入一个头文件。
定义 read 和 print 函数:
1 istream &read(istream &is, Sales_data &item){//从给定流中将数据读到给定的对象里 2 double price = 0; 3 is >> item.book_no >> item.units_sold >> price; 4 item.revenue = price * item.units_sold; 5 return is; 6 } 7 8 ostream &print(ostream &os, const Sales_data &item){//将给定对象的内容打印到给定的流中 9 os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price(); 10 //print 函数不负责换行。一般来说执行输出的函数应该尽量减少对格式的控制,这样可以确保由用户代码来决定是否换行 11 return os; 12 }
需要注意的是:read 和 print 分别接受各自 IO 类型的引用作为其参数,因为 IO 类属于不能被拷贝的类型,因此我们只能通过引用来传递它们。
定义 add 函数:
1 Sales_data add(const Sales_data &lhs, const Sales_data &rhs){ 2 Sales_data sum = lhs;//默认情况下拷贝的是数据成员 3 sum.combine(rhs);//把 rhs 的数据成员加到 sum 中 4 return sum;//返回 sum 的副本 5 }
至此,我们一开始设计的类 Sales_data 就算是完善啦:
1 #include <iostream> 2 using namespace std; 3 4 struct Sales_data{ 5 //数据成员 6 std::string book_no; 7 unsigned units_sold = 0; 8 double revenue = 0.0; 9 10 //函数成员 11 std::string isbn() const { 12 return book_no; 13 // return this->book_no;//等价语句 14 } 15 Sales_data& combine(const Sales_data&); 16 double avg_price() const; 17 }; 18 //Sales_data的非成员函数声明 19 Sales_data add(const Sales_data&, const Sales_data&); 20 std::ostream &print(std::ostream&, const Sales_data&); 21 std::istream &read(std::istream&, Sales_data&); 22 23 double Sales_data::avg_price() const{ 24 if(units_sold) return revenue / units_sold; 25 return 0; 26 } 27 28 Sales_data& Sales_data::combine(const Sales_data &rhs){ 29 units_sold += rhs.units_sold; 30 revenue += rhs.revenue; 31 return *this; 32 } 33 34 istream &read(istream &is, Sales_data &item){//从给定流中将数据读到给定的对象里 35 double price = 0; 36 is >> item.book_no >> item.units_sold >> price; 37 item.revenue = price * item.units_sold; 38 return is; 39 } 40 41 ostream &print(ostream &os, const Sales_data &item){//将给定对象的内容打印到给定的流中 42 os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price(); 43 //print 函数不负责换行。一般来说执行输出的函数应该尽量减少对格式的控制,这样可以确保由用户代码来决定是否换行 44 return os; 45 } 46 47 Sales_data add(const Sales_data &lhs, const Sales_data &rhs){ 48 Sales_data sum = lhs;//默认情况下拷贝的是数据成员 49 sum.combine(rhs);//把 rhs 的数据成员加到 sum 中 50 return sum;//返回 sum 的副本 51 } 52 53 int main(void){ 54 Sales_data total, x; 55 read(cin, total); 56 while(read(cin, x)){ 57 total = add(total, x); 58 print(cout, total); 59 cout << endl; 60 } 61 return 0; 62 }
注意:我们这里定义类使用的 struct 而没有用 class 关键字。实际上 struct 和 class 仅仅只是形式上有所不同而已,我们可以用这两个关键字中的任何一个定义类。唯一一点区别是 struct 和 class 的默认访问权限不太一样。(c++ primer 第五版 240 页)