一、类的其他特性
1、类成员再探
1)定义一个类型成员
除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名。由类定义的类型名字和其他成员一样存在访问限制,可以是public或者private中的一种。用来定义类型别名的成员必须先定义后使用,因此,类型成员通常出现在类开始的地方。
1 // 一个窗口类 2 class Screen 3 { 4 public: 5 typedef std::string::size_type pos; 6 private: 7 pos m_cursor = 0; // 光标位置 8 pos m_height = 0, m_width = 0; 9 std::string m_contents; 10 };
2)令成员作为内联函数
定义在类内部的成员函数是自动inline的。我们可以在类内部把inline作为声明的一部分显示地声明成员函数,同样的,也能在类的外部用inline关键字修饰函数的定义。
1 #ifndef SCREEN_H 2 #define SCREEN_H 3 4 #include <string> 5 6 // 一个窗口类 7 class Screen 8 { 9 public: 10 typedef std::string::size_type pos; 11 Screen() = default; 12 Screen(pos ht, pos wd, char c) :m_height(ht), m_width(wd), m_contents(ht*wd, c){} 13 char get() const { return m_contents[m_cursor]; } // 隐式内联 14 inline char get(pos r, pos c) const; // 显示内联 15 Screen &move(pos r, pos c); // 能在之后被设为内联 16 private: 17 pos m_cursor = 0; // 光标位置 18 pos m_height = 0, m_width = 0; 19 std::string m_contents; 20 }; 21 22 #endif
1 #include "Screen.h" 2 3 inline Screen &Screen::move(pos r, pos c) // 可在函数的定义处指定inline 4 { 5 pos row = r * m_width; 6 m_cursor = row + c; 7 return *this; 8 } 9 10 char Screen::get(pos r, pos c) const // 在类的内部声明成inline 11 { 12 pos row = r * c; 13 return m_contents[row + c]; 14 }
我们无须在声明和定义的地方同时说明inline,但这么做是合法的。不过最好只在类外部定义的地方说明inline,这样更容易理解。
3)可变数据成员
有时会发生一种情况,希望能修改类的某个数据成员,即使是在一个const成员函数内。可以通过在变量的声明中加入mutable关键字做到。
一个可变数据成员永远不会是const,即使它是const对象的成员。因此一个const成员函数可以修改一个可变成员的值。
1 class Screen 2 { 3 public: 4 void some_member() const; 5 void print() const{ 6 std::cout << m_access_ctr << std::endl; 7 } 8 private: 9 mutable size_t m_access_ctr = 0; // 即使在一个const对象内也能被修改 10 }; 11 void Screen::some_member()const 12 { 13 ++m_access_ctr; 14 }
2、类类型
即使两个类的成员列表完全一致,它们也是不同的类型。对于一个类来说,它的成员和其他任何类(或者任何其他作用域)的成员都不是一回事。
1)类的声明
就像函数的声明和定义分离开来一样,我们也能仅声明类而暂时不定义它:
1 class Screen;
这种声明有时被称作前向声明,它向程序中引入了名字Screen并且指明Screen是一种类类型。对于类型Screen来说,在它声明之后定义之前是一个不完全类型,也就是说,此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员。
不完全类型只能在非常有限的情况下使用:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。
因为只有当类全部完成后类才算被定义,所以一个类的成员不能是类自己。然而,一旦一个类的名字出现后,它就被认为是声明过了(但尚未定义),因此类允许包含指向它自身类型的引用或指针。
3、友元
1)友元类
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。友元关系不具有传递性。
1 class Screen 2 { 3 friend class Window_mgr; // 友元类 4 public: 5 typedef std::string::size_type pos; 6 Screen() = default; 7 Screen(pos ht, pos wd, char c) :m_height(ht), m_width(wd), m_contents(ht*wd, c){} 8 private: 9 pos m_cursor = 0; // 光标位置 10 pos m_height = 0, m_width = 0; 11 std::string m_contents; 12 }; 13 14 class Window_mgr 15 { 16 public: 17 using ScreenIndex = std::vector<Screen>::size_type; 18 Window_mgr() :m_screens(10, Screen()){} 19 void clear(ScreenIndex); 20 private: 21 std::vector<Screen> m_screens; 22 }; 23 24 void Window_mgr::clear(ScreenIndex i) 25 { 26 Screen &s = m_screens[i]; 27 s.m_contents = std::string(s.m_height*s.m_width, ' '); 28 }
2)成员函数作为友元
当把一个成员函数声明成为友元时,我们必须明确指出该成员函数属于哪一个类:
1 class Screen; 2 class Window_mgr 3 { 4 public: 5 using ScreenIndex = std::vector<Screen>::size_type; 6 void clear(ScreenIndex); 7 Window_mgr(); 8 private: 9 std::vector<Screen> m_screens; 10 }; 11 12 // 一个窗口类 13 class Screen 14 { 15 friend void Window_mgr::clear(Window_mgr::ScreenIndex); // 友元类 16 public: 17 typedef std::string::size_type pos; 18 Screen() = default; 19 Screen(pos ht, pos wd, char c) :m_height(ht), m_width(wd), m_contents(ht*wd, c){} 20 private: 21 pos m_cursor = 0; // 光标位置 22 pos m_height = 0, m_width = 0; 23 std::string m_contents; 24 }; 25 26 Window_mgr::Window_mgr(){ 27 m_screens = std::vector<Screen>(10, Screen()); 28 } 29 30 void Window_mgr::clear(ScreenIndex i) 31 { 32 Screen &s = m_screens[i]; 33 s.m_contents = std::string(s.m_height*s.m_width, ' '); 34 }
注意上述声明出现的位置。
3)函数重载和友元
尽管重载函数的名字相同,但它们仍然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明。
4)友元声明和作用域
类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域中是可见的。然而,友元本身不一定真的声明在当前作用域中。
甚至就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。换句话说,即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的。
二、类的作用域
1、作用域和定义在类外部的成员
一个类就是一个作用域的事实能够很好地解释为什么当我们在类的外部定义成员函数时必须同时提供类名和函数名。在类的外部,成员的名字被隐藏起来了。
一旦遇到了类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体。因此,我们可以直接使用类的其他成员而无需再次授权了。
函数的返回类型通常出现在函数名之前。因此,当成员函数定义在类的外部时,返回类型中使用的都位于类的作用域之外。这时,返回类型必须指明它是哪个类的成员。
2、名字查找与类的作用域
名字查找的过程:
a、首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明。
b、如果没找到,继续查找外层作用域。
c、如果最终没有找到匹配的声明,则程序报错。
对于定义在类内部的成员函数来说,解析其中名字的方式与上述的查找规则有所区别,类的定义分两步处理:
a、首先,编译成员的声明。
b、直到类全部可见后才编译函数体。
这种两阶段的处理方式只适用于成员函数中使用的名字。声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在编译前确保可见。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找。
一般来说,内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过。然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
成员函数中使用的名字按如下方式解析:
a、首先,在成员函数内查找该名字的声明。和前面一样,只有在函数使用之前出现的声明才被考虑。
b、如果在成员函数内没有找到,则在类中继续查找,这时所有的成员都可以被考虑。
c、如果类内也没找到该名字的声明,在成员函数定义之前的外部作用域继续查找。
三、构造函数再探
1、构造函数初始值列表
如果没有在构造函数的初始值列表中显示地初始化成员,则该成员将在构造函数体之前执行默认初始化。
在构造函数体内执行的是成员的赋值,有时我们可以忽略数据成员初始化和赋值之间的差异,但并非总能这样。如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初始值。
在构造函数初始值列表中每个成员只能出现一次。成员的初始化顺序与它们在类定义中的出现顺序一致,构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
2、委托构造函数
C++11新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。
和其他构造函数一样,一个委托构造函数也有一个成员初始值的列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员初始值列表一样,类名后面紧跟圆括号后面括起来的参数列表,参数列表必须与类中另外一个构造函数匹配。
当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。如果受委托的构造函数体包含代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。
1 #include <iostream> 2 #include <string> 3 4 class SalesData 5 { 6 public: 7 SalesData(std::string s, unsigned cnt, double price): 8 m_book_no(s), m_units_sold(cnt), m_revenue(cnt*price){ 9 std::cout << "QAQ" << std::endl; 10 } 11 SalesData() :SalesData("", 0, 0){ 12 std::cout << "hello" << std::endl; 13 } 14 15 private: 16 std::string m_book_no; 17 unsigned m_units_sold = 0; 18 double m_revenue = 0.0; 19 }; 20 int main() 21 { 22 SalesData sd; 23 return 0; 24 }
3、默认构造函数的作用
当对象被默认初始化或值初始化时自动执行默认构造函数。
如果想定义一个使用默认构造函数进行初始化的对象,对象名之后不要带上空的括号对,带上空的括号对是定义了一个函数而非对象。
4、隐式的类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数。
编译器只会自动地执行一步类类型转换。
1 #include <iostream> 2 #include <string> 3 4 class SalesData 5 { 6 public: 7 SalesData() :m_book_no(""), m_units_sold(0), m_revenue(0){} 8 SalesData(const std::string &s) :m_book_no(s), m_units_sold(0), m_revenue(0){} 9 SalesData(const std::string &s, unsigned n, double p) : 10 m_book_no(s), m_units_sold(n), m_revenue(p * n){} 11 12 SalesData &combine(const SalesData rhs) 13 { 14 m_units_sold += rhs.m_units_sold; 15 m_revenue += rhs.m_revenue; 16 return *this; // 返回调用该函数的对象 17 } 18 private: 19 std::string m_book_no; // 书名 20 unsigned m_units_sold; // 数量 21 double m_revenue; // 总价 22 }; 23 int main() 24 { 25 SalesData sd; 26 sd.combine(std::string("998")); // string隐式的转换成SalesData 27 // sd.combine("998"); // 错误,有2步转换 28 return 0; 29 }
在要求隐式转换的上下文中,我们可以通过将函数声明成explicit加以阻止。关键字explicit只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit的。只能在类内声明函数时使用explicit,在类外部定义时不应重复。
1 #include <iostream> 2 #include <string> 3 4 class SalesData 5 { 6 public: 7 SalesData() :m_book_no(""), m_units_sold(0), m_revenue(0){} 8 explicit SalesData(const std::string &s) :m_book_no(s), m_units_sold(0), m_revenue(0){} 9 SalesData(const std::string &s, unsigned n, double p) : 10 m_book_no(s), m_units_sold(n), m_revenue(p * n){} 11 12 SalesData &combine(const SalesData rhs) 13 { 14 m_units_sold += rhs.m_units_sold; 15 m_revenue += rhs.m_revenue; 16 return *this; // 返回调用该函数的对象 17 } 18 private: 19 std::string m_book_no; // 书名 20 unsigned m_units_sold; // 数量 21 double m_revenue; // 总价 22 }; 23 int main() 24 { 25 SalesData sd; 26 sd.combine(std::string("998")); // 错误 27 return 0; 28 }
explicit构造函数只能用于直接初始化。
1 #include <iostream> 2 #include <string> 3 4 class SalesData 5 { 6 public: 7 SalesData() :m_book_no(""), m_units_sold(0), m_revenue(0){} 8 explicit SalesData(const std::string &s) :m_book_no(s), m_units_sold(0), m_revenue(0){} 9 SalesData(const std::string &s, unsigned n, double p) : 10 m_book_no(s), m_units_sold(n), m_revenue(p * n){} 11 12 SalesData &combine(const SalesData rhs) 13 { 14 m_units_sold += rhs.m_units_sold; 15 m_revenue += rhs.m_revenue; 16 return *this; // 返回调用该函数的对象 17 } 18 private: 19 std::string m_book_no; // 书名 20 unsigned m_units_sold; // 数量 21 double m_revenue; // 总价 22 }; 23 int main() 24 { 25 std::string book = "998"; 26 SalesData sd(book); // 直接初始化 27 //SalesData sd=book; // 错误,拷贝初始化 28 return 0; 29 }
尽管编译器不会将explicit的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显示地强制进行转换:
1 #include <iostream> 2 #include <string> 3 4 class SalesData 5 { 6 public: 7 SalesData() :m_book_no(""), m_units_sold(0), m_revenue(0){} 8 explicit SalesData(const std::string &s) :m_book_no(s), m_units_sold(0), m_revenue(0){} 9 SalesData(const std::string &s, unsigned n, double p) : 10 m_book_no(s), m_units_sold(n), m_revenue(p * n){} 11 12 SalesData &combine(const SalesData rhs) 13 { 14 m_units_sold += rhs.m_units_sold; 15 m_revenue += rhs.m_revenue; 16 return *this; // 返回调用该函数的对象 17 } 18 private: 19 std::string m_book_no; // 书名 20 unsigned m_units_sold; // 数量 21 double m_revenue; // 总价 22 }; 23 int main() 24 { 25 std::string book = "998"; 26 SalesData sd; 27 sd.combine(SalesData(book)); 28 sd.combine(static_cast<SalesData>(book)); 29 return 0; 30 }
5、聚合类
聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足以下条件时,我们就说它是聚合的:
a、所有成员都是public的。
b、没有定义任何构造函数。
c、没有类内初始值。
d、没有基类,也没有虚函数。
我们可以提供一个花括号括起来的成员初始值列表来初始化聚合类的数据成员,初始值的顺序必须与声明的顺序一致。与初始化数组元素的规则一样,如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。
1 #include <iostream> 2 #include <string> 3 4 class Data 5 { 6 public: 7 int val; 8 std::string s; 9 }; 10 int main() 11 { 12 Data d = { 233, "hello" }; 13 std::cout << d.val << "," << d.s << std::endl; 14 return 0; 15 }
6、字面值常量类
除了算术类型、引用和指针外,某些类也是字面值类型。数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合下列要求,则它也是一个字面值常量类:
a、数据成员都必须是字面值类型。
b、类必须至少含有一个constexpr构造函数。
c、如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某一种类型,则初始值必须使用成员自己的constexpr构造函数。
d、类必须使用析构函数的默认定义,该成员负责销毁类的对象。
尽管构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr函数。事实上,一个字面值常量类必须至少提供一个constexpr构造函数。constexpr构造函数可以声明成default的形式(或者是删除函数的形式);否则,constexpr构造函数体一般来说应该是空的。
1 #include <iostream> 2 #include <string> 3 4 class Base { 5 public: 6 constexpr Base():x(1024){} 7 private: 8 int x; 9 }; 10 int main() 11 { 12 constexpr Base b; 13 return 0; 14 }
四、类的静态成员
1、声明静态成员
我们通过在成员的声明之前加上关键字static使得其与类关联在一起。静态成员可以是public或private的。静态数据成员的类型可以是常量、引用、指针、类类型等。
静态成员函数不与任何对象绑在一起,它们不包含this指针。因此,静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针。
2、使用静态成员
使用作用域运算符直接访问静态成员。虽然静态成员不属于某个对象,但是我们仍然可以使用类的对象、引用或指针来访问静态成员。成员函数不用通过作用域运算符就能直接使用静态成员。
3、定义静态成员
我们既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句。
因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。这意味着它们不是由类的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。一个静态数据成员只能定义一次。
类似于全局变量,静态数据成员定义在任何函数体之外。因此一旦它被定义,就将一直存在于程序的整个生命周期。
1 #include <iostream> 2 #include <string> 3 4 class Account 5 { 6 public: 7 void cal() 8 { 9 amount += amount * interestRate; 10 } 11 static double rate() { return interestRate; } 12 static void rate(double); 13 private: 14 std::string owner; 15 double amount; 16 static double interestRate; 17 static double initRate(){ return 0.5; } 18 }; 19 20 // 从类名开始,这条语句的剩余部分就都位于类的作用域之内了。因此我们可以直接使用initRate()。 21 // 注意虽然initRate是私有的,我们也能使用它来初始化interestRate 22 double Account::interestRate = initRate(); 23 24 void Account::rate(double newRate) 25 { 26 interestRate = newRate; 27 } 28 29 int main() 30 { 31 std::cout << Account::rate() << std::endl; 32 return 0; 33 }
4、静态成员的类内初始化
通常情况下,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式,因为这些成员本身就是常量表达式,所以它们能用在所有适合于常量表达式的地方。
1 class Account 2 { 3 private: 4 static constexpr int period = 30; 5 double arr[period]; 6 };
如果在类的内部为静态成员提供了一个初始值,则成员的定义不能在指定一个初始值了:
1 class Account 2 { 3 private: 4 static constexpr int period = 30; 5 double arr[period]; 6 }; 7 constexpr int period; // 初始值在类的内部提供
即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。
5、静态成员能用于某些场景,而普通成员不能
静态数据成员的类型可以就是它所属的类类型。而非静态数据成员则受到限制,只能声明成所属类的指针或引用。
静态成员和普通成员的另一个差别是我们可以使用静态成员作为默认实参。
1 #include <iostream> 2 #include <string> 3 4 class Account 5 { 6 public: 7 Account(int v = bg) :val(bg){} 8 static void show() 9 { 10 std::cout << "static:mem.val " << mem.val << std::endl; 11 std::cout << "static:bg " << bg << std::endl; 12 } 13 void showVal() 14 { 15 std::cout << val << std::endl; 16 } 17 private: 18 static Account mem; 19 static int bg; 20 int val; 21 }; 22 23 int Account::bg = 998; 24 Account Account::mem = Account(999); 25 26 int main() 27 { 28 Account ac; 29 ac.show(); 30 ac.showVal(); 31 return 0; 32 }