声明、定义

  1. // 类是一个作用域。
  2. // 编译器编译类分两步:
  3. // 1、编译成员(变量、函数)声明
  4. // 2、编译成员函数体。
  5. struct Sales_data { // 也可以用class,没有深层次的区别,仅仅是默认访问权限不同。
  6. public:
  7. // 以下是类成员函数,必须在类内声明,但定义可在类外部
  8. // 在类内部定义,隐式声明为inline
  9. // const 修饰 this,即为const (Sale_data* const),括号内是this的类型。
  10. // 这个const修饰的函数称为常量成员函数
  11. std::string isbn() const {
  12. // 隐式调用return this.bookNo;
  13. // this表示当前调用这个函数的对象,Sale_data* const类型,常量指针。
  14. return bookNo;
  15. }
  16. Sale_data& combine(const Sales_data&);
  17. double avg_price() const; // 平均价格
  18. private:
  19. std::string bookNo; // isbn编号
  20. unsigned units_sold = 0; // 销量,类内初始值,注意不是初始化(在构造函数完成)
  21. double revenue = 0.0; // 总销售收入,类内初始值。
  22. }
  23. // 类外部定义成员函数
  24. double Sales_data::avg_price() const { // const必须和声明时一样。
  25. if(units_sold) return revenue / units_sold;
  26. return 0;
  27. }
  28. // combine是为了实现类似+=复合赋值运算符,应尽量模仿+=,因此返回引用。
  29. Sales_data& Sales_data::combine(const Sales_data& rhs){
  30. units_sold += rhs.units_sold;
  31. revenue += rhs.revenue;
  32. return *this; // *this返回引用
  33. }
  34. // 非类成员函数,在类外部声明
  35. // 这些函数接口被类成员函数使用,因此声明在同一个头文件中。
  36. Sales_data add(const Sales)data&, const Sales_data&);
  37. std::ostream& print(std::ostream&, const Sales_data&);
  38. std::istream& read(std::istream&, Sales_data);

构造函数

初始化类对象数据成员(成员变量)的成员函数,只要对象被创建,就会执行构造函数进行初始化。

  1. // 构造函数名必须和类名相同,不能有返回值。
  2. // 可以有不同构造函数(重载,区别在形参列表上)。
  3. Sales_data::Sales_data(......) // 不能有const声明
  4. : bookNo("") // 构造函数初始值列表,可为空(走默认构造函数初始化逻辑)
  5. , units_sold(0) // 没有在列表中的,走默认构造函数初始化逻辑。
  6. , revenue(0.0)
  7. {
  8. // 这是很不可取的成员初始化方法,这并不是真正的初始化,仅仅是初始值列表之后的赋值。
  9. bookNo = "";
  10. units_sold = 0;
  11. revenue = 0.0;
  12. }

默认构造函数

当没有显式定义构造函数,编译器就会隐式创建一个合成的默认构造函数(synthesized default constructor),反之不会合成。
默认构造函数不一定是可靠的,比如有些类成员默认初始化后的值是未定义的。如含有内置类型、复合类型成员(数组、指针)。
如果有任何成员变量不能进行默认初始化,则不会合成默认构造函数。比如类类型成员变量,没有默认构造函数。

  1. // 默认构造函数:没有初始化列表,形参列表为空。
  2. // 如何初始化成员:
  3. // 1、如果有类内初始值,就执行类内初始化
  4. // 2、否则执行默认初始化
  5. Sales_data::Sales_data(){
  6. }

当对象被默认初始化或值初始化时,自动调用默认构造函数。触发默认初始化的情况:

  1. // 触发情形1(默认初始化):不适用初始值定义个非静态变量/数组。
  2. T t;
  3. T arr[100];
  4. // 触发情形2(默认初始化):含有类类型成员,且该类型有合成默认构造函数
  5. // 触发情形3(默认初始化):没有在初始值列表中显示初始化,也没有类内初始值。

触发值初始化的情况:

  1. // 触发情形1(值初始化):初始值数量少于数组大小
  2. T t[5] = {1, 2, 3, 4};
  3. // 触发情形2(值初始化):没有初始化值定义一个局部静态变量
  4. static T t; //
  5. // 触发情形3(值初始化):显式请求值初始化
  6. T t(......); // 如vector、string等容器。

注意!!!成员变量的初始化顺序取决于在类内的声明顺序,而不是初始值列表中的出现顺序。

  1. struct Sales_data {
  2. // 显示定义一个默认构造函数,等同于合成的默认构造函数。
  3. Sales_data() = default;
  4. Sales_data(const std::string& isbn)
  5. : bookNo // 构造函数初始值列表
  6. {}
  7. Sales_data(const std::string& isbn, unsign n, double p)
  8. : bookNo(s), units_sold(n), revenue(p*n) // 构造函数初始值列表
  9. {}
  10. Sales_data(std::istream&);
  11. }

委托构造函数

delegating constructor。

  1. class Sales_data {
  2. public:
  3. // 非委托构造函数使用对应的实参初始化成员
  4. Sales_data(std::string s, unsigned cnt, double price)
  5. : bookNo(s)
  6. , units_sold(cnt)
  7. , revenue(cnt*price)
  8. {
  9. }
  10. // 冒号后面的都是委托构造函数。
  11. Sales_data() : Sales_data("", 0, 0) {}
  12. Sales_data(string s) : Sales_data(s, 0, 0) {}
  13. Sales_data(istream& is) : Sales_data(){
  14. read(is, *this);
  15. }
  16. }

转换构造函数

converting constructor。只有一个形参的构造函数。

  1. class T {
  2. public:
  3. T(int i);
  4. T(std::string str);
  5. }
  6. void fuck(T t){
  7. }
  8. int main(){
  9. T t = 1; // int 转换成T类型。
  10. fuck(1); // int 转换成T类型。
  11. }

但转换只能一步,如果不能一步转换到位,则表示不能转换。

  1. class T {
  2. public:
  3. T(std::string str);
  4. }
  5. int main(){
  6. T t = "90823"; // 错误,"90823"是字符串常量,并不是string类型。不能一次转换到位。
  7. }

我们可以阻止这种隐式转换:

  1. class T {
  2. public:
  3. explicit T(int i); // explicity关键字,将函数声明为显式的,阻止隐式转换。
  4. }
  5. int main(){
  6. T t = 1; // 错误,T(int i)是explicit(显式的)
  7. T t(1); // 正确,显式初始化。
  8. }

拷贝、赋值、析构(待完成)

访问控制

C++的类就好比一个人,他可以为外界(类的用户)提供服务和帮助(public成员),也会有自己(类内部)的私人空间(private),还有与家人(继承)共享的空间(protected),还有与朋友(友元)共享的空间(friend)。

public&private

  1. // 访问说明符:public、proected、private
  2. class Sales_data {
  3. void print(); // private,因为class的默认访问权限是private
  4. // struct的默认访问权限是public,这是struct和class的唯一区别。
  5. public: // 整个程序内可被访问
  6. ......
  7. public: // 不限制数量
  8. ......
  9. private: // 只能被类成员函数访问
  10. ......
  11. public: // 不限制顺序
  12. ......
  13. protected:
  14. ......
  15. }

友元friend

  1. class Sales_data {
  2. // 友元声明,这些非成员函数可以访问对象的非public成员。
  3. // 这里默认add在作用域可见,所以add的声明可以在后面
  4. // 一般在类开头、结尾的地方集中声明
  5. // 这不是函数声明
  6. friend void add(const Sample&);
  7. // 类的友元声明,同函数的友元声明
  8. // 在FriendClass定义前,必须先声明Sample
  9. friend class FriendClass;
  10. // 类成员函数的友元声明,同函数友元声明。
  11. // 在func的定义前,必须先声明Sample
  12. friend void FriendClass::func();
  13. }
  14. // 除了以上的友元声明之外,还需要进行一次友元函数的声明,使友元函数对类可见。
  15. // 这并不是普通意义上的声明,它的作用是影响访问权限,使得友元函数可在类内部可见。
  16. Sample add(const Sample&);
  17. std::istream &read(Sample&);
  18. std::ostream &print(Sample&);
  19. // 或者是extern声明,定义实现在外部。
  20. extern Sample add(const Sample&);
  21. extern std::istream &read(Sample&);
  22. extern std::ostream &print(Sample&);

以下例子展示友元声明的影响:

  1. struct X {
  2. friend void f() { // 极端例子,友元直接定义在类内部
  3. };
  4. X() { f(); } // 错误,f还未被声明
  5. void g();
  6. void h();
  7. }
  8. void X::g() { f(); } // 错误,f还未被声明
  9. void f(); // 友元函数的函数声明
  10. void X::h() { f(); } // 这才正确了。

注意以上友元声明限制规则并不是所有编译器都支持。

可变数据成员mutable

有少部分情况,需求在const常量成员函数中也能修改成员值,这就需要用到mutable特性。

  1. class T {
  2. Sample& fuckmutable() const {
  3. b = 1; // 错误:const成员函数内不能修改成员
  4. a = {"a","b","c"}; // 正确:可以修改mutable成员
  5. return *this; // const函数,返回的是const Sample&类型
  6. }
  7. private:
  8. // mutable:可变数据成员声明
  9. mutable std:string a;
  10. unsigned int b = 0; // 类内初始值,编译器不一定支持。
  11. }

成员初始值

C++11标准之后,为了一个类成员设置初始值的最好做法是类内初始值。在所有构造函数中都会执行此初始化。

  1. class T {
  2. private:
  3. unsigned b = 0; // 类内初始值,不一定编译器支持。
  4. double c = 0.0;
  5. int shit{0}; // 类内初始值,要么=,要么{}
  6. const int ci; // 必须在构造初始值列表中初始化
  7. int &ri; // 必须在构造初始值列表中初始化
  8. static int sd; // 声明一个静态成员变量,保存在程序内存中的数据区
  9. // 生命周期和程序相同。
  10. //静态成员变量一般不要在类内初始化。
  11. static constexpr int period = 30;
  12. }

前向声明

  1. struct Fucker; // Fucker类的前向声明(forward declaration)
  2. // 在声明之后,定义之前,Fucker,知道是类,但是不知道内部情况。
  3. Fucker *pFucker; // 不完全类型可用于指针
  4. Fucker &refFucker; // 不完全类型可用于引用
  5. Fucker &func(Fucker); // 不完全类型可用于函数参数,返回值
  6. struct Sample {
  7. Sample sample; // 错误,此时还是不完全类型。只能是指针引用、函数参数/返回值。
  8. Sample& sample; // 正确。
  9. }

名字查找

一般的名字查找过程:

  • 在名字所在块内查找声明语句,只考虑名字使用之前出现的声明。
  • 没有找到,则在外层作用域查找
  • 还有没有找到匹配,就报错。

    名字查找(成员声明中)

    只适用于成员中使用的名字,首先会在类内查找,没找到则继续在类所在作用域查找。 ```cpp

typedef double Money; string bal; class Account { public:

  1. // 在balance成员函数声明中使用到了Money返回类型。
  2. // 1、在类内寻找该类型的声明,在Money使用之前,即成员声明之前,
  3. // 2、显然没有找到,到Account类所在作用域查找,找到了Money。
  4. Money balance() { return bal; }

private: Money bal; // … }

  1. 特殊情况:一般内层作用域的名字会覆盖外层作用域,但在类中却就有不同,如果先使用了外层的某个名字,然后再重新定义一个同名的类型,则会报错。
  2. ```cpp
  3. typedef double Money;
  4. class Account {
  5. public:
  6. Money balance() { return bal; } // 使用外层作用域的Money
  7. double balance() { // 假如是这个成员函数,则不会触发下面的错误。
  8. Money m = bal;
  9. return m;
  10. }
  11. private:
  12. typedef double Money; // 错误:不能重新定义Money,前面声明中已经使用了Money
  13. // 注意是声明中,不是定义中。
  14. Money bal;
  15. }

名字查找(成员定义中)

成员函数定义中的名字查找:

  1. 在成员函数内,名字使用前查找
  2. 没找到,查找类内所有成员
  3. 特殊情况,若成员定义在函数外部,还要考虑定义之前的全局作用域中查找。
  4. 没有找到,在成员函数定义之前的作用域(外层作用域)查找。

针对特殊情况c:

  1. class T {
  2. public:
  3. void fuck();
  4. }
  5. typedef double Double;
  6. void T::fuck(){
  7. Double d = 1.0; // 特殊情况c,在此定义之前的全局作用域中找到了Double的声明。
  8. return d;
  9. }

静态成员static

  • 静态成员变量
  • 静态成员函数 ```cpp

class Account { public: void calculate() { amount += amount * rate; } // 滚利

  1. // 静态成员成员,不能使用this,不能const声明
  2. static double rate() { return rate; } // 当前储蓄利率
  3. static void rate(double); // 设置储蓄利率

private: double amount; // 当前储蓄额

  1. static double rate; // 当前利率
  2. static double initRate(); // 初始利率

}

// 所有静态成员不属于类对象,不能在构造函数初始化,而需要在类外部定义或初始化静态成员。 double Account::rate = initRate(); // 这是在定义和初始化rate

// 类外部定义静态成员函数 Account::rate(double r){ rate = r; }

int main(){ double r = Account::rate(); // 访问静态成员函数

  1. Account acnt, *acnt1 = &acnt;
  2. acnt.rate(); // 访问静态函数
  3. acnt1->rate(); // 访问静态函数

}

  1. <a name="fQNss"></a>
  2. ## 类内初始化
  3. 一般不会在类内初始化静态成员。满足以下条件才能类内初始化静态变量:
  4. - 静态变量必须是字面值常量类型的constexpr
  5. - 初始值必须是constexpr,常量表达式
  6. ```cpp
  7. class T {
  8. private:
  9. static int s = 1; // 错误,s必须是constexpr类型
  10. static const s = 1; // 正确
  11. static constexpr int s = 1; // 正确
  12. static string str = "1"; // 错误,str不是字面值类型constexpr
  13. static const string str = "1"; // 错误,同上
  14. static constexpr string str = "1"; // 错误, 同上。
  15. }
  16. // 如果s要类外定义,则必须按如下形式
  17. constexpr int T::s;

成员函数引用限定符(const、&、&&)

  1. class Foo {
  2. public:
  3. void fuck() &; // *this必须是一个左值引用
  4. void fuck() &&; // *this必须是一个右值引用
  5. void fuck() const; // *this必须是const的
  6. Foo bitch() & const; // 错误:const限定符必须在前
  7. Foo bitch() const &; // 正确,*this既是const的又是&的。
  8. };
  9. void Foo::fuck1() & { // 声明和定义中都必须有相同引用限定符。
  10. }
  11. void Foo::fuck2() const {
  12. }
  13. Foo Foo::bitch2() const & {
  14. }
  15. Foo a, b, &c = a;
  16. a.fuck1(); //错误,a必须是Foo&类型
  17. b.fuck2(); //错误;b必须是const Foo类型
  18. c.fucke(); //正确。

final关键字

final关键字有两个用途:

  • 阻止被继承
  • 阻止虚函数被重写 ```cpp

class Base final { // final用途一:阻止Base被其他类继承 public:

  1. virtual void func1() final; // final用途二:阻止虚函数func1被派生类重写

}

class Derived : public Base { // 错误:Base is final class public: virtual void func1() { // 错误:func1 is final class …… }
}

``` 合理利用final可以提高性能,因为有些编译器会认为被声明为final的类是不可能再向下存在多态,完全可以优化掉查找vtable的工作(虚函数指针查找)。