5、this调用语句必须是构造函数中的第一个可执行语句_C++知识点总结 可用作查漏补缺...

本文总结了大部分C++的进阶知识,可作为手册查阅,内容参考自清华大学郑莉教授的C++课程。

内联函数

声明时使用关键字 inline

编译时在调用处用函数体进行替换,节省了参数传递、控制转移等开销

注意:

内联函数体内不能有循环语句和switch语句

内联函数的定义必须出现在内联函数第一次被调用之前

对内联函数不能进行异常接口声明

定义内联函数,可以显式用inline声明,也可以直接在类内定义好实现

编译器并不一定遵从我们的inline

constexpr函数

constexpr修饰的函数在其所有参数都是constexpr时,一定返回constexpr

函数体中必须有且仅有一条return语句

constexpr的变量的值必须是编译器在编译的时候就可以确定的

constexpr int get_size() { return 20; }
constexpr int foo = get_size();  //正确:foo是一个常量表达式

重载函数

通过形参的个数不同或者类型不同进行区分

无法通过返回值区分

构造函数

默认构造函数

//下面两个都是默认构造函数,如在类中同时出现,将产生编译错误:
Clock();
Clock(int newH=0,int newM=0,int newS=0);

隐含生成的构造函数

如果程序中未定义构造函数,编译器将在需要时自动生成一个默认构造函数

参数列表为空,不为数据成员设置初始值

如果类内定义了成员的初始值,则使用类内定义的初始值

如果没有定义类内的初始值,则以默认方式初始化

基本类型的数据默认初始化的值是不确定的

=default

如果程序中已定义构造函数,默认情况下编译器就不再隐含生成默认构造函数。如果此时依然希望编译器隐含生成默认构造函数,可以使用=default

class Clock {
public:
   Clock() =default; //指示编译器提供默认构造函数
   Clock(int newH, int newM, int newS);     //构造函数
private:
   int hour, minute, second;
};

委托构造函数

类中往往有多个构造函数,只是参数表和初始化列表不同,其初始化算法都是相同的,这时,为了避免代码重复,可以使用委托构造函数

不使用委托构造函数:

//构造函数
Clock(int newH, int newM, int newS) : hour(newH),minute(newM),second(newS) {}
//默认构造函数
Clock() : hour(0),minute(0),second(0) {}

使用委托构造函数:

Clock(int newH, int newM, int newS) : hour(newH),minute(newM),second(newS) {}
Clock(): Clock(0, 0, 0) {}

复制构造函数

复制构造函数是一种特殊的构造函数,其形参为本类的对象引用,作用是用一个已存在的对象去初始化同类型的新对象

定义一个对象时,以本类另一个对象作为初始值,发生复制构造

如果函数的形参是类的对象,调用函数时,将使用实参对象初始化形参对象,发生复制构造

如果函数的返回值是类的对象,函数执行完成返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生复制构造

隐含的复制构造函数

如果程序员没有为类声明拷贝初始化构造函数,则编译器自己生成一个隐含的复制构造函数

这个构造函数执行的功能是:用作为初始值的对象的每个数据成员的值,初始化将要建立的对象的对应数据成员(浅拷贝)

=delete

C++11做法:用=delete指示编译器不生成默认复制构造函数。

class Point {   //Point 类的定义
public:
    Point(int xx=0, int yy=0) { x = xx; y = yy; }    //构造函数,内联
    Point(const Point& p) =delete;  //指示编译器不生成默认复制构造函数
private:
    int x, y; //私有数据
};

类的组合

构造组合类对象时的初始化次序

首先对构造函数初始化列表中列出的成员(包括基本类型成员和对象成员)进行初始化,初始化次序是成员在类体中定义的次序

成员对象构造函数调用顺序:按对象成员的声明顺序,先声明者先构造

初始化列表中未出现的成员对象:调用用默认构造函数(即无形参的)初始化

处理完初始化列表之后,再执行构造函数的函数体

前向引用声明

如果需要在某个类的声明之前,引用该类,则应进行前向引用声明

前向引用声明只为程序引入一个标识符,但具体声明在其他地方

class B;  //前向引用声明

class A {
public:
  void f(B b);
};

class B {
public:
  void g(A a);
};

使用前向引用声明虽然可以解决一些问题,但它并不是万能的

在提供一个完整的类声明之前,不能声明该类的对象,也不能在内联成员函数中使用该类的对象

当使用前向引用声明时,只能使用被声明的符号,而不能涉及类的任何细节

class Fred; //前向引用声明

class Barney {
   Fred x; //错误:类Fred的声明尚不完善
};

class Fred {
   Barney y;
};

联合体

成员共用同一组内存单元

任何两个成员不会同时有效

class ExamInfo {
private:
    string name;    //课程名称
    enum { GRADE, PASS, PERCENTAGE } mode;//计分方式
    union {
        char grade; //等级制的成绩
        bool pass;  //只记是否通过课程的成绩
        int percent;    //百分制的成绩
    };
public:
    //三种构造函数,分别用等级、是否通过和百分初始化
    ExamInfo(string name, char grade)
        : name(name), mode(GRADE), grade(grade) { }
    ExamInfo(string name, bool pass)
        : name(name), mode(PASS), pass(pass) { }
    ExamInfo(string name, int percent)
        : name(name), mode(PERCENTAGE), percent(percent) { }
    void show();
}

void ExamInfo::show() {
    cout << name << ": ";
    switch (mode) {
      case GRADE: cout << grade;  break;
      case PASS: cout << (pass ? "PASS" : "FAIL"); break;
      case PERCENTAGE: cout << percent; break;
    }
    cout << endl;
}

int main() {
    ExamInfo course1("English", 'B');
    ExamInfo course2("Calculus", true);
    ExamInfo course3("C++ Programming", 85);
    course1.show();
    course2.show();
    course3.show();
    return 0;
}

//运行结果:
//English: B
//Calculus: PASS
//C++ Programming: 85

枚举类

//enum class 枚举类型名: 底层类型 {枚举值列表};
enum class Type { General, Light, Medium, Heavy};
enum class Type: char { General, Light, Medium, Heavy};
enum class Category { General=1, Pistol, MachineGun, Cannon};

枚举类的优势

强作用域,其作用域限制在枚举类中

转换限制,枚举类对象不可以与整型隐式地互相转换。

可以指定底层类型

#include<iostream>
using namespace std;

enum class Side{ Right, Left };
enum class Thing{ Wrong, Right };  //不冲突

int main()
{
    Side s = Side::Right;
    Thing w = Thing::Wrong;
    cout << (s == w) << endl;  //编译错误,无法直接比较不同枚举类
    return 0;
}

类的友元

友元是C++提供的一种破坏数据封装和数据隐藏的机制

通过将一个模块声明为另一个模块的友元,一个模块能够引用到另一个模块中本是被隐藏的信息

为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元

友元函数

友元函数是在类声明中由关键字friend修饰说明的非成员函数,在它的函数体中能够通过对象名访问 private 和protected成员

作用:增加灵活性,使程序员可以在封装和快速性方面做合理选择

访问对象中的成员必须通过对象名

友元类

若一个类为另一个类的友元,则此类的所有成员都能访问对方类的私有成员

声明语法:将友元类名在另一个类中使用friend修饰说明

类的友元关系是单向的

如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有、保护数据

常类型

对于既需要共享、又需要防止改变的数据应该声明为常类型(用const进行修饰)

const关键字可以被用于参与对重载函数的区分

通过常对象只能调用它的常成员函数

#include<iostream>
using namespace std;

class R {
public:
  R(int r1, int r2) : r1(r1), r2(r2) { }
  void print();
  void print() const;
private:
  int r1, r2;
};

void R::print() {
  cout << r1 << ":" << r2 << endl;
}

void R::print() const {
  cout << r1 << ";" << r2 << endl;
}

int main() {
  R a(5,4);
  a.print(); //调用void print()
  const R b(20,52); 
  b.print(); //调用void print() const
  return 0;
}

常成员函数可以被非常对象调用,但常对象不可调用非常成员函数

多文件结构

外部变量

如果一个变量除了在定义它的源文件中可以使用外,还能被其它文件使用,那么就称这个变量是外部变量

文件作用域中定义的变量,默认情况下都是外部变量,但在其它文件中如果需要使用这一变量,需要用extern关键字加以声明

外部函数

在所有类之外声明的函数(也就是非成员函数),都是具有文件作用域的

这样的函数都可以在不同的编译单元中被调用,只要在调用之前进行引用性声明(即声明函数原型)即可。也可以在声明函数原型或定义函数时用extern修饰,其效果与不加修饰的默认状态是一样的

编译预处理指令

预处理在编译前进行

每条预处理指令必须单独占用一行

预处理指令可以出现在程序的任何位置

指针

空值nullptr

  • 以往用0或者NULL去表达空指针的问题:

C/C++的NULL宏是个被有很多潜在BUG的宏。因为有的库把其定义成整数0,有的定义成 (void*)0。在C的时代还好。但是在C++的时代,这就会引发很多问题

  • C++11使用nullptr关键字,是表达更准确,类型安全的空指针

指向常量的指针

不能通过指向常量的指针改变所指对象的值,但指针本身可以改变,可以指向另外的对象。

int a;
const int *p1 = &a; //p1是指向常量的指针
int b;
p1 = &b; //正确,p1本身的值可以改变
*p1 = 1; //编译时出错,不能通过p1改变所指的对象

指针类型的常量

若声明指针常量,则指针本身的值不能被改变。

int a;
int * const p2 = &a;
p2 = &b; //错误,p2是指针常量,值不能改变

函数指针

int f(int a, int b) {
    return a + b;
}

int main() {
   int (*p)(int, int) = f;
   cout<<p(1, 2)<<endl;
   return 0;
}

智能指针

显式管理内存在是能上有优势,但容易出错

C++11提供智能指针的数据类型,对垃圾回收技术提供了一些支持,实现一定程度的内存管理

  • unique_ptr :不允许多个指针共享资源,可以用标准库中的move函数转移指针
  • shared_ptr :多个指针共享资源
  • weak_ptr :可复制shared_ptr,但其构造或者释放对资源不产生影响

移动构造

移动构造可以减少不必要的复制,带来性能上的提升

C++11之前,如果要将源对象的状态转移到目标对象只能通过复制。在某些情况下,我们没有必要复制对象——只需要移动它们

有可被利用的临时对象时,触发移动构造

//函数返回含有指针成员的对象
//将要返回的局部对象转移到主调函数,省去了构造和删除临时对象的过程
#include<iostream>
using namespace std;

class IntNum {
public:
IntNum(int x = 0) : xptr(new int(x)){ //构造函数
    cout << "Calling constructor..." << endl;
}
IntNum(const IntNum & n) : xptr(new int(*n.xptr)){//复制构造函数
    cout << "Calling copy constructor..." << endl;
}
//&&是右值引用
//函数返回的临时变量是右值
IntNum(IntNum && n): xptr(n.xptr){ //移动构造函数
    n.xptr = nullptr;
    cout << "Calling move constructor..." << endl;
}
~IntNum(){ //析构函数
    delete xptr;
    cout << "Destructing..." << endl;
}

private:
int *xptr;
};

//返回值为IntNum类对象
IntNum getNum() {
    IntNum a;
    return a;
}

int main() {
    cout << getNum().getInt() << endl; return 0;
}

/*
运行结果:
Calling constructor...
Calling move constructor...
Destructing...  //这里释放了nullptr
0
Destructing...
*/

左值和右值

左值和右值都是针对表达式而言的

左值是指表达式结束后依然存在的持久对象

右值指表达式结束时就不再存在的临时对象——显然右值不可以被取地址

读入字符串

用cin的>>操作符输入字符串,会以空格作为分隔符,空格后的内容会在下一回输入时被读取

getline可以输入整行字符串(要包string头文件),例如:getline(cin, s2);

输入字符串时,可以使用其它分隔符作为字符串结束的标志(例如逗号、分号),将分隔符作为getline的第3个参数即可,例如:getline(cin, s2, ',');

#include <iostream>
#include <string>

using namespace std;

int main() {
    for (int i = 0; i < 2; i++){
        string city, state;
        getline(cin, city, ',');
        getline(cin, state);
        cout << "City:" << city << “ State:" << state << endl;
    }
    return 0;   
}

/*
运行结果:
Beijing,China
City: Beijing State: China
San Francisco,the United States
City: San Francisco State: the United States
*/

继承

公有继承(public)

继承的访问控制

  • 基类的public和protected成员:访问属性在派生类中保持不变
  • 基类的private成员:不可直接访问

访问权限

  • 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员
  • 通过派生类的对象:只能访问public成员

私有继承(private)

继承的访问控制

  • 基类的publicprotected成员:都以private身份出现在派生类中
  • 基类的private成员:不可直接访问

访问权限

  • 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员
  • 通过派生类的对象:不能直接访问从基类继承的任何成员

保护继承(protected)

继承的访问控制

  • 基类的publicprotected成员:都以protected身份出现在派生类中
  • 基类的private成员:不可直接访问

访问权限

  • 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员
  • 通过派生类的对象:不能直接访问从基类继承的任何成员

protected 成员的特点与作用

  • 对建立其所在类对象的模块来说,它与 private 成员的性质相同
  • 对于其派生类来说,它与 public 成员的性质相同
  • 既实现了数据隐藏,又方便继承,实现代码重用
class A{
public:
    void setA(int);
private:
    int a;
};
class B{
public:
    void setB(int);
private:
    int b;
};
class C:public A, private B{
public:
    void setC(int, int, int);
private:
    int c;
};
void A::setA(int x){
    a = x;
}
void B::setB(int x){
    b = x;
}
void C::setC(int x, int y, int z){
    setA(x);
    setB(y);
    c = z;
}
int main(int argc, const char * argv[]) {
    C obj;
    obj.setA(5); // 正确
    obj.setB(6); // 错误
    obj.setC(6, 7, 9); // 正确
    return 0;
}

派生类的构造函数

默认情况

  • 基类的构造函数不被继承
  • 派生类需要定义自己的构造函数

C++11规定

  • 可用using语句继承基类构造函数
  • 但是只能初始化从基类继承的成员

派生类新增成员可以通过类内初始值进行初始化

  • 语法形式:

using B::B;

多继承且有对象成员时派生的构造函数定义语法

派生类名::派生类名(形参表):
基类名1(参数), 基类名2(参数), ..., 基类名n(参数), 
本类成员(含对象成员)初始化列表
{
        //其他初始化
};

构造函数的执行顺序

  1. 调用基类构造函数

顺序按照它们被继承时声明的顺序(从左向右)

  1. 对初始化列表中的成员进行初始化

顺序按照它们在类中定义的顺序

对象成员初始化时自动调用其所属类的构造函数,由初始化列表提供参数

  1. 执行派生类的构造函数体中的内容

派生类复制构造函数

派生类未定义复制构造函数的情况

编译器会在需要时生成一个隐含的复制构造函数

先调用基类的复制构造函数

再为派生类新增的成员执行复制

派生类定义了复制构造函数的情况

一般都要为基类的复制构造函数传递参数

复制构造函数只能接受一个参数,既用来初始化派生类定义的成员,也将被传递给基类的复制构造函数

基类的复制构造函数形参类型是基类对象的引用,实参可以是派生类对象的引用

例如: C::C(const C &c1): B(c1) {…}

派生类的析构函数

析构函数不被继承,派生类如果需要,要自行声明析构函数

声明方法与无继承关系时类的析构函数相同

不需要显式地调用基类的析构函数,系统会自动隐式调用

先执行派生类析构函数的函数体,再调用基类的析构函数

访问从基类继承的成员

当派生类与基类中有相同成员时:

  • 若未特别限定,则通过派生类对象使用的是派生类中的同名成员
  • 如要通过派生类对象访问基类中被隐藏的同名成员,应使用基类名和作用域操作符(::)来限定

如果从不同基类继承了同名成员,但是在派生类中没有定义同名成员,“派生类对象名或引用名.成员名”、“派生类指针->成员名”访问成员存在二义性问题

  • 解决方式:用类名限定

虚基类

需要解决的问题

  • 当派生类从多个基类派生,而这些基类又共同基类,则在访问此共同基类中的成员时,将产生冗余,并有可能因冗余带来不一致性

虚基类声明

  • 以virtual说明基类继承方式
  • 例:class B1:virtual public B

作用

  • 主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题
  • 为最远的派生类提供唯一的基类成员,而不重复产生多次复制

注意:

  • 在第一级继承时就要将共同基类设计为虚基类

虚基类及其派生类构造函数

建立对象时所指定的类称为最远派生类

虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的

在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中为虚基类的构造函数列出参数。如果未列出,则表示调用该虚基类的默认构造函数

在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略

#include <iostream>
using namespace std;

class Base0 {   
public:
    Base0(int var) : var0(var) { }
    int var0;
    void fun0() { cout << "Member of Base0" << endl; }
};
class Base1: virtual public Base0 {
public: 
    Base1(int var) : Base0(var) { }
    int var1;
};
class Base2: virtual public Base0 { 
public:
    Base2(int var) : Base0(var) { }
    int var2;
};

class Derived: public Base1, public Base2 {
public:
    Derived(int var) : Base0(var), Base1(var), Base2(var) { }
    int var;
    void fun() 
   { cout << "Member of Derived" << endl; }
};

int main() {    //程序主函数
    Derived d(1);
    d.var0 = 2; //直接访问虚基类的数据成员
    d.fun0();   //直接访问虚基类的函数成员
    return 0;
}

运算符重载

双目运算符重载规则

如果要重载 B 为类成员函数,使之能够实现表达式 oprd1 B oprd2,其中 oprd1 为A 类对象,则 B 应被重载为 A 类的成员函数,形参类型应该是 oprd2 所属的类型

经重载后,表达式 oprd1 B oprd2 相当于 oprd1.operator B(oprd2)

#include <iostream>
using namespace std;

class Complex {
public:
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
    //运算符+重载成员函数
    Complex operator + (const Complex &c2) const;
    //运算符-重载成员函数
    Complex operator - (const Complex &c2) const;
    void display() const;   //输出复数
private:
    double real;    //复数实部
    double imag;    //复数虚部
};
//复数类加减法运算重载为成员函数
Complex Complex::operator + (const Complex &c2) const{
  //创建一个临时无名对象作为返回值 
  return Complex(real+c2.real, imag+c2.imag); 
}

Complex Complex::operator - (const Complex &c2) const{
 //创建一个临时无名对象作为返回值
    return Complex(real-c2.real, imag-c2.imag); 
}

void Complex::display() const {
    cout<<"("<<real<<", "<<imag<<")"<<endl;
}
//复数类加减法运算重载为成员函数
int main() {
    Complex c1(5, 4), c2(2, 10), c3;
    cout << "c1 = "; c1.display();
    cout << "c2 = "; c2.display();
    c3 = c1 - c2;   //使用重载运算符完成复数减法
    cout << "c3 = c1 - c2 = "; c3.display();
    c3 = c1 + c2;   //使用重载运算符完成复数加法
    cout << "c3 = c1 + c2 = "; c3.display();
    return 0;
}

前置单目运算符重载规则

如果要重载 U 为类成员函数,使之能够实现表达式 U oprd,其中 oprd 为A类对象,则 U 应被重载为 A 类的成员函数,无形参。

经重载后,表达式 U oprd 相当于 oprd.operator U()

后置单目运算符 ++和--重载规则

如果要重载 ++或--为类成员函数,使之能够实现表达式 oprd++ 或 oprd-- ,其中 oprd 为A类对象,则 ++或-- 应被重载为 A 类的成员函数,且具有一个 int 类型形参。

经重载后,表达式 oprd++ 相当于 oprd.operator ++(0)

#include <iostream>
using namespace std;

class Clock {//时钟类定义
public: 
    Clock(int hour = 0, int minute = 0, int second = 0);
    void showTime() const;
  //前置单目运算符重载
    Clock& operator ++ ();
  //后置单目运算符重载
    Clock operator ++ (int);    
private:
    int hour, minute, second;
};

Clock::Clock(int hour, int minute, int second) {    
    if (0 <= hour && hour < 24 && 0 <= minute && minute < 60
        && 0 <= second && second < 60) {
        this->hour = hour;
        this->minute = minute;
        this->second = second;
    } else
        cout << "Time error!" << endl;
}
void Clock::showTime() const {  //显示时间
    cout << hour << ":" << minute << ":" << second << endl;
}

//重载前置++和后置++为时钟类成员函数
Clock & Clock::operator ++ () { 
    second++;
    if (second >= 60) {
        second -= 60;  minute++;
        if (minute >= 60) {
          minute -= 60; hour = (hour + 1) % 24;
        }
    }
    return *this;
}

Clock Clock::operator ++ (int) {
    //注意形参表中的整型参数
    Clock old = *this;
    ++(*this);  //调用前置“++”运算符
    return old;
}

int main() {
    Clock myClock(23, 59, 59);
    cout << "First time output: ";
    myClock.showTime();
    cout << "Show myClock++:    ";
    (myClock++).showTime();
    cout << "Show ++myClock:    ";
    (++myClock).showTime();
    return 0;
}

运算符重载为非成员函数

有些运算符不能重载为成员函数,例如二元运算符的左操作数不是对象,或者是不能由我们重载运算符的对象

运算符重载为非成员函数的规则

  • 函数的形参代表依自左至右次序排列的各操作数
  • 参数个数=原操作数个数(后置++、--除外)
  • 至少应该有一个自定义类型的参数
  • 后置单目运算符 ++和--的重载函数,形参列表中要增加一个int,但不必写形参名
  • 如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该类的友元
  • 双目运算符 B重载后,表达式oprd1 B oprd2等同于operator B(oprd1,oprd2 )
  • 前置单目运算符 B重载后,表达式 B oprd等同于operator B(oprd )
  • 后置单目运算符 ++和--重载后,表达式 oprd B等同于operator B(oprd,0 )
//重载Complex的加减法和“<<”运算符为非成员函数
//将+、-(双目)重载为非成员函数,并将其声明为复数类的友元,两个操作数都是复数类的常引用。 • 将<<(双目)重载为非成员函数,并将其声明为复数类的友元,它的左操作数是std::ostream引用,右操作数为复数类的常引用,返回std::ostream引用
#include <iostream>
using namespace std;

class Complex {
    public:
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }  
    friend Complex operator+(const Complex &c1, const Complex &c2);
    friend Complex operator-(const Complex &c1, const Complex &c2);
    friend ostream & operator<<(ostream &out, const Complex &c);
    private:    
    double real;  //复数实部
    double imag;  //复数虚部
};

Complex operator+(const Complex &c1, const Complex &c2){
    return Complex(c1.real+c2.real, c1.imag+c2.imag); 
}
Complex operator-(const Complex &c1, const Complex &c2){
    return Complex(c1.real-c2.real, c1.imag-c2.imag); 
}

ostream & operator<<(ostream &out, const Complex &c){
    out << "(" << c.real << ", " << c.imag << ")";
    return out;
}

int main() {    
    Complex c1(5, 4), c2(2, 10), c3;    
    cout << "c1 = " << c1 << endl;
    cout << "c2 = " << c2 << endl;
    c3 = c1 - c2;   //使用重载运算符完成复数减法
    cout << "c3 = c1 - c2 = " << c3 << endl;
    c3 = c1 + c2;   //使用重载运算符完成复数加法
    cout << "c3 = c1 + c2 = " << c3 << endl;
    return 0;
}

虚函数

  • 用virtual关键字说明的函数
  • 虚函数是实现运行时多态性基础
  • C++中的虚函数是动态绑定的函数
  • 虚函数必须是非静态的成员函数,虚函数经过派生之后,就可以实现运行过程中的多态
  • 一般成员函数可以是虚函数
  • 构造函数不能是虚函数
  • 析构函数可以是虚函数
  • 虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候
  • 在派生类中可以对基类中的成员函数进行覆盖
  • 虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的

virtual 关键字

  • 派生类可以不显式地用virtual声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数:
  • 该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型
  • 该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针、引用型的返回值
  • 如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数
  • 派生类中的虚函数还会隐藏基类中同名函数的所有其它重载形式
  • 一般习惯于在派生类的函数中也使用virtual关键字,以增加程序的可读性

虚析构函数

为什么需要虚析构函数? - 可能通过基类指针删除派生类对象; - 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数成为虚函数,否则执行delete的结果是不确定的

#include <iostream>
using namespace std;
class Base{
public:
    virtual ~Base();
};
Base::~Base()
{
  cout << "Base ";
}
class Derived: public Base{
public:
    virtual ~Derived();
};
Derived::~Derived(){
    cout << "Derived ";
}
void fun(Base *b){
    delete b;
}
int main(int argc, const char * argv[]) {
    Base *b = new Derived();
    fun(b);
    return 0;
}

虚表与动态绑定

虚表

  • 每个多态类有一个虚表(virtual table)
  • 虚表中有当前类的各个虚函数的入口地址
  • 每个对象有一个指向当前类的虚表的指针(虚指针vptr)

动态绑定的实现

  • 构造函数中为对象的虚指针赋值
  • 通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址
  • 通过该入口地址调用虚函数
class A{
public:
    virtual void fun();
};
//在32位机器上,sizeof(A)为:4;在64位机器上,sizeof(A)为:8
//因为A中含有一个指向虚表的指针,在32位机器上,指针占4个字节;在64位机器上,指针占8个字节

抽象类和纯虚函数

纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:virtual 函数类型 函数名(参数表) = 0;

带有纯虚函数的类称为抽象类

抽象类作用

  • 抽象类为抽象和设计的目的而声明
  • 将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为
  • 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现

注意:

  • 抽象类只能作为基类来使用。
  • 不能定义抽象类的对象。
#include <iostream>
using namespace std;

class Base1 { 
public:
    virtual void display() const = 0;   //纯虚函数
};

class Base2: public Base1 { 
public:
    virtual void display() const; //覆盖基类的虚函数
};
void Base2::display() const {
    cout << "Base2::display()" << endl;
}

class Derived: public Base2 { 
public:
     virtual void display() const; //覆盖基类的虚函数
};
void Derived::display() const {
    cout << "Derived::display()" << endl;
} 
void fun(Base1 *ptr) { 
    ptr->display(); 
}
int main() {    
    Base2 base2;    
    Derived derived;    
    fun(&base2);    
    fun(&derived);  
    return 0;
}

override

C++11 引入显式函数覆盖,在编译期而非运行期捕获此类错误。 - 在虚函数显式重载中运用,编译器会检查基类是否存在一虚拟函数,与派生类中带有声明override的虚拟函数,有相同的函数签名(signature);若不存在,则会回报错误

  • 多态行为的基础:基类声明虚函数,继承类声明一个函数覆盖该虚函数
  • 覆盖要求: 函数签名(signatture)完全一致
  • 函数签名包括:函数名 参数列表 const

final

C++11提供final,用来避免类被继承,或是基类的函数被改写 例:

struct Base1 final { };
struct Derived1 : Base1 { }; // 编译错误:Base1为final,不允许被继承
struct Base2 { virtual void f() final; };
struct Derived2 : Base2 { void f(); // 编译错误:Base2::f 为final,不允许被覆盖 };

模板

函数模板

语法形式:

template <模板参数表>

模板参数表的内容:

  • 类型参数:class(或typename) 标识符
  • 常量参数:类型说明符 标识符
  • 模板参数:template <参数表> class标识符

注意:

  • 一个函数模板并非自动可以处理所有类型的数据
  • 只有能够进行函数模板中运算的类型,可以作为类型实参
  • 自定义的类,需要重载模板中的运算符,才能作为类型实参
#include <iostream>
using namespace std;

template <class T>  //定义函数模板
void outputArray(const T *array, int count) {
    for (int i = 0; i < count; i++)
        cout << array[i] << " "; //如果数组元素是类的对象,需要该对象所属类重载了流插入运算符“<<”
    cout << endl;
}

int main() {     
    const int A_COUNT = 8, B_COUNT = 8, C_COUNT = 20;
    int a [A_COUNT] = { 1, 2, 3, 4, 5, 6, 7, 8 };
    double b[B_COUNT] = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8 };
    char c[C_COUNT] = "Welcome!";

    cout << " a array contains:" << endl;
    outputArray(a, A_COUNT);    
    cout << " b array contains:" << endl;
    outputArray(b, B_COUNT);    
    cout << " c array contains:" << endl;
    outputArray(c, C_COUNT);    
    return 0;
}

类模板

使用类模板使用户可以为类声明一种模式,使得类中的某些数据成员、某些成员函数的参数、某些成员函数的返回值,能取任意类型(包括基本类型的和用户自定义类型)

类模板 template <模板参数表> class 类名 {类成员声明};

如果需要在类模板以外定义其成员函数,则要采用以下的形式: template <模板参数表> 类型名 类名<模板参数标识符列表>::函数名(参数表)

#include <iostream>
#include <cstdlib>
using namespace std;
struct Student {
  int id;       //学号
  float gpa;    //平均分
}; 
template <class T>
class Store {//类模板:实现对任意类型数据进行存取
private:
    T item; // item用于存放任意类型的数据
    bool haveValue;  // haveValue标记item是否已被存入内容
public:
    Store();
    T &getElem();   //提取数据函数
    void putElem(const T &x);  //存入数据函数
};

template <class T>  
Store<T>::Store(): haveValue(false) { } 
template <class T>
T &Store<T>::getElem() {
    //如试图提取未初始化的数据,则终止程序
    if (!haveValue) {   
        cout << "No item present!" << endl;
        exit(1);    //使程序完全退出,返回到操作系统。
    }
    return item;        // 返回item中存放的数据 
}
template <class T>
void Store<T>::putElem(const T &x) {
    // 将haveValue 置为true,表示item中已存入数值   
    haveValue = true;   
    item = x;           // 将x值存入item
}

int main() {
    Store<int> s1, s2;  
    s1.putElem(3);  
    s2.putElem(-7);
    cout << s1.getElem() << "  " << s2.getElem() << endl;

    Student g = { 1000, 23 };
    Store<Student> s3;
    s3.putElem(g); 
    cout << "The student id is " << s3.getElem().id << endl;

    Store<double> d;
    cout << "Retrieving object D... ";
    cout << d.getElem() << endl;
   //d未初始化,执行函数D.getElement()时导致程序终止
    return 0;
}

数组类模板

自己实现一个动态数组

#ifndef ARRAY_H
#define ARRAY_H
#include <cassert>

template <class T>  //数组类模板定义
class Array {
private:
    T* list;        //用于存放动态分配的数组内存首地址
    int size;       //数组大小(元素个数)
public:
    Array(int sz = 50);     //构造函数
    Array(const Array<T> &a);   //复制构造函数
    ~Array();           //析构函数
    Array<T> & operator = (const Array<T> &rhs);    //重载"=“
    T & operator [] (int i); //重载"[]”
    const T & operator [] (int i) const;     //重载"[]”常函数
    operator T * ();        //重载到T*类型的转换
    operator const T * () const;
    int getSize() const;        //取数组的大小
    void resize(int sz);        //修改数组的大小
};

template <class T> Array<T>::Array(int sz) {//构造函数
    assert(sz >= 0);//sz为数组大小(元素个数),应当非负
    size = sz;  // 将元素个数赋值给变量size
    list = new T [size];    //动态分配size个T类型的元素空间
}

template <class T> Array<T>::~Array() { //析构函数
    delete [] list;
}

template <class T> 
Array<T>::Array(const Array<T> &a) {    //复制构造函数
    size = a.size;     //从对象x取得数组大小,并赋值给当前对象的成员
    list = new T[size]; // 动态分配n个T类型的元素空间
    for (int i = 0; i < size; i++)     //从对象X复制数组元素到本对象 
        list[i] = a.list[i];
}

//重载"="运算符,将对象rhs赋值给本对象。实现对象之间的整体赋值
template <class T>
Array<T> &Array<T>::operator = (const Array<T>& rhs) {
    if (&rhs != this) {
        //如果本对象中数组大小与rhs不同,则删除数组原有内存,然后重新分配
        if (size != rhs.size) {
            delete [] list; //删除数组原有内存
            size = rhs.size;    //设置本对象的数组大小
            list = new T[size];  //重新分配size个元素的内存
        }
        //从对象X复制数组元素到本对象  
        for (int i = 0; i < size; i++)
            list[i] = rhs.list[i];
    }
    return *this;   //返回当前对象的引用
}

//重载下标运算符,实现与普通数组一样通过下标访问元素,具有越界检查功能
template <class T>
T &Array<T>::operator[] (int n) {
    assert(n >= 0 && n < size);  //检查下标是否越界
    return list[n];       //返回下标为n的数组元素
}

template <class T>
const T &Array<T>::operator[] (int n) const {
    assert(n >= 0 && n < size);  //检查下标是否越界
    return list[n];       //返回下标为n的数组元素
}

//重载指针转换运算符,将Array类的对象名转换为T类型的指针
template <class T>
Array<T>::operator T * () {
    return list;    //返回当前对象中私有数组的首地址
}

//取当前数组的大小
template <class T>
int Array<T>::getSize() const {
    return size;
}

// 将数组大小修改为sz
template <class T>
void Array<T>::resize(int sz) {
    assert(sz >= 0);    //检查sz是否非负
    if (sz == size) //如果指定的大小与原有大小一样,什么也不做
        return;
    T* newList = new T [sz];    //申请新的数组内存
    int n = (sz < size) ? sz : size;//将sz与size中较小的一个赋值给n
    //将原有数组中前n个元素复制到新数组中
    for (int i = 0; i < n; i++)
        newList[i] = list[i];
    delete[] list;      //删除原数组
    list = newList; // 使list指向新数组
    size = sz;  //更新size
}
#endif  //ARRAY_H

泛型程序设计与STL

迭代器

迭代器是算法和容器的桥梁

  • 迭代器用作访问容器中的元素
  • 算法不直接操作容器中的数据,而是通过迭代器间接操作

算法和容器独立

  • 增加新的算法,无需影响容器的实现
  • 增加新的容器,原有的算法也能适用
#include <algorithm>
#include <iterator>
#include <vector>
#include <iostream>
using namespace std;

//将来自输入迭代器的n个T类型的数值排序,将结果通过输出迭代器result输出
template <class T, class InputIterator, class OutputIterator>
void mySort(InputIterator first, InputIterator last, OutputIterator result) {
    //通过输入迭代器将输入数据存入向量容器s中
    vector<T> s;
    for (;first != last; ++first)
        s.push_back(*first);
    //对s进行排序,sort函数的参数必须是随机访问迭代器
    sort(s.begin(), s.end());  
    copy(s.begin(), s.end(), result);   //将s序列通过输出迭代器输出
}

int main() {
    //将s数组的内容排序后输出
    double a[5] = { 1.2, 2.4, 0.8, 3.3, 3.2 };
    mySort<double>(a, a + 5, ostream_iterator<double>(cout, " "));
    cout << endl;
    //从标准输入读入若干个整数,将排序后的结果输出
    mySort<int>(istream_iterator<int>(cin), istream_iterator<int>(), ostream_iterator<int>(cout, " "));
    cout << endl;
    return 0;
}

逆向迭代器

  • rbegin() :指向容器尾的逆向迭代器
  • rend():指向容器首的逆向迭代器

逆向迭代器的类型名的表示方式如下:

  • S::reverse_iterator:逆向迭代器类型
  • S::const_reverse_iterator:逆向常迭代器类型

函数对象

一个行为类似函数的对象

可以没有参数,也可以带有若干参数

其功能是获取一个值,或者改变操作的状态

普通函数就是函数对象

重载了“()”运算符的类的实例是函数对象

#include <iostream>
#include <numeric> //包含数值算法头文件
using namespace std;
class MultClass{  //定义MultClass类
public:
  //重载操作符operator()
    int operator() (int x, int y) const { return x * y; }   
};
int main() {
    int a[] = { 1, 2, 3, 4, 5 };
    const int N = sizeof(a) / sizeof(int);
    cout << "The result by multipling all elements in a is "
        << accumulate(a, a + N, 1, MultClass()) //将类multclass传递给通用算法
        << endl;
    return 0;
}
#include<functional>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

int main() {
    int intArr[] = { 30, 90, 10, 40, 70, 50, 20, 80 };
    const int N = sizeof(intArr) / sizeof(int);
    vector<int> a(intArr, intArr + N);
    cout << "before sorting:" << endl;
    copy(a.begin(),a.end(),ostream_iterator<int>(cout,"t"));
    cout << endl;

    sort(a.begin(), a.end(), greater<int>()); //STL中的二元谓词函数对象

    cout << "after sorting:" << endl;
    copy(a.begin(),a.end(),ostream_iterator<int>(cout,"t"));
    cout << endl;
    return 0;
}

I/O流

操纵符(manipulator)

//使用width控制输出宽度
#include <iostream>
using namespace std;

int main() {
    double values[] = { 1.23, 35.36, 653.7, 4358.24 };
    for(int i = 0; i < 4; i++) {
        cout.width(10);
        cout << values[i] << endl;
    }
    return 0;
}
/*
输出结果:
      1.23
     35.36
     653.7
   4358.24
   */
//使用setw操纵符指定宽度
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;

int main() {
    double values[] = { 1.23, 35.36, 653.7, 4358.24 };
    string names[] = { "Zoot", "Jimmy", "Al", "Stan" };
    for (int i = 0; i < 4; i++)
      cout << setw(6) << names[i] 
     << setw(10) << values[i] << endl;
    return 0;
}
/*
输出结果:
  Zoot      1.23
 Jimmy     35.36
    Al     653.7
  Stan   4358.24
  */
//设置对齐方式
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;

int main() {
    double values[] = { 1.23, 35.36, 653.7, 4358.24 };
    string names[] = { "Zoot", "Jimmy", "Al", "Stan" };
    for (int i=0;i<4;i++)
      cout << setiosflags(ios_base::left)//左对齐
           << setw(6) << names[i]
           << resetiosflags(ios_base::left)
           << setw(10) << values[i] << endl;
    return 0;
}
/*
输出结果:
Zoot        1.23
Jimmy      35.36
Al         653.7
Stan     4358.24
*/
//控制输出精度——未指定fixed或scientific
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;

int main() {
    double values[] = { 1.23, 35.36, 653.7, 4358.24 };
    string names[] = { "Zoot", "Jimmy", "Al", "Stan" };
    for (int i=0;i<4;i++)
      cout << setiosflags(ios_base::left)
        << setw(6) << names[i]
        << resetiosflags(ios_base::left)//清除左对齐设置
        << setw(10) << setprecision(1) << values[i] << endl;
    return 0;
}
/*
输出结果:
Zoot           1
Jimmy     4e+001
Al        7e+002
Stan      4e+003
*/
//控制输出精度——指定fixed
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
int main() {
    double values[] = { 1.23, 35.36, 653.7, 4358.24 };
    string names[] = { "Zoot", "Jimmy", "Al", "Stan" };
    cout << setiosflags(ios_base::fixed);   
    for (int i=0;i<4;i++)
      cout << setiosflags(ios_base::left)
        << setw(6) << names[i]
        << resetiosflags(ios_base::left)//清除左对齐设置
        << setw(10) << setprecision(1) << values[i] << endl;
    return 0;
}
输出结果:
Zoot         1.2
Jimmy       35.4
Al         653.7
Stan      4358.2
//控制输出精度——指定scientific
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
int main() {
    double values[] = { 1.23, 35.36, 653.7, 4358.24 };
    string names[] = { "Zoot", "Jimmy", "Al", "Stan" };
  cout << setiosflags(ios_base::scientific);
    for (int i=0;i<4;i++)
      cout << setiosflags(ios_base::left)
        << setw(6) << names[i]
        << resetiosflags(ios_base::left)//清除左对齐设置
        << setw(10) << setprecision(1) << values[i] << endl;
    return 0;
}
输出结果:
Zoot    1.2e+000
Jimmy   3.5e+001
Al      6.5e+002
Stan    4.4e+003

二进制文件流

使用ofstream构造函数中的模式参量指定二进制输出模式或以通常方式构造一个流,然后使用setmode成员函数,在文件打开后改变模式

//向二进制文件输出
#include <fstream>
using namespace std;
struct Date { 
    int mon, day, year;  
};
int main() {
    Date dt = { 6, 10, 92 };
    ofstream file("date.dat", ios_base::binary);
    file.write(reinterpret_cast<char *>(&dt),sizeof(dt));
    file.close();
    return 0;
}

字符串输出流( ostringstream )

将字符串作为输出流的目标,可以实现将其他数据类型转换为字符串的功能

//用ostringstream将数值转换为字符串
#include <iostream>
#include <sstream>
#include <string>
using namespace std;

//函数模板toString可以将各种支持“<<“插入符的类型的对象转换为字符串。
template <class T>
inline string toString(const T &v) {
    ostringstream os;   //创建字符串输出流
    os << v;        //将变量v的值写入字符串流
    return os.str();    //返回输出流生成的字符串
}

int main() {
    string str1 = toString(5);
    cout << str1 << endl;
    string str2 = toString(1.2);
    cout << str2 << endl;
    return 0;
}
/*
输出结果:
5
1.2
*/

输入流

重要的输入流类

  • istream类最适合用于顺序文本模式输入,cin是其实例
  • ifstream类支持磁盘文件输入
  • istringstream类支持从内存中的字符串输入
//get函数应用举例
#include <iostream>
using namespace std;
int main() {
    char ch;
    while ((ch = cin.get()) != EOF)
        cout.put(ch);
    return 0;
}
//为输入流指定一个终止字符
#include <iostream>
#include <string>
using namespace std;
int main() {
    string line;
    cout << "Type a line terminated by 't' " << endl; 
    getline(cin, line, 't');
    cout << line << endl;
    return 0;
}
//从文件读一个二进制记录到一个结构中
#include <iostream>
#include <fstream>
#include <cstring>
using namespace std;

struct SalaryInfo {
    unsigned id;
    double salary;
}; 
int main() {
    SalaryInfo employee1 = { 600001, 8000 };
    ofstream os("payroll", ios_base::out | ios_base::binary);
    os.write(reinterpret_cast<char *>(&employee1), sizeof(employee1));
    os.close();
    ifstream is("payroll", ios_base::in | ios_base::binary);
    if (is) {
        SalaryInfo employee2;
        is.read(reinterpret_cast<char *>(&employee2), sizeof(employee2));
        cout << employee2.id << " " << employee2.salary << endl;
    } else {
        cout << "ERROR: Cannot open file 'payroll'." << endl;
    }
    is.close();
    return 0;
}
//用seekg函数设置位置指针
int main() {
    int values[] = { 3, 7, 0, 5, 4 };
    ofstream os("integers", ios_base::out | ios_base::binary);
    os.write(reinterpret_cast<char *>(values), sizeof(values));
    os.close();

    ifstream is("integers", ios_base::in | ios_base::binary);
    if (is) {
        is.seekg(3 * sizeof(int));
        int v;
        is.read(reinterpret_cast<char *>(&v), sizeof(int));
        cout << "The 4th integer in the file 'integers' is " << v << endl;
    } else {
        cout << "ERROR: Cannot open file 'integers'." << endl;
    }
    return 0;
}
//读一个文件并显示出其中0元素的位置
int main() {
    ifstream file("integers", ios_base::in | ios_base::binary);
    if (file) {
        while (file) {//读到文件尾file为0
            streampos here = file.tellg();
            int v;
            file.read(reinterpret_cast<char *>(&v), sizeof(int));
            if (file && v == 0) 
            cout << "Position " << here << " is 0" << endl;
        }
    } else {
        cout << "ERROR: Cannot open file 'integers'." << endl;
    }
    file.close();
    return 0;
}

字符串输入流( istringstream)

将字符串作为文本输入流的源,可以将字符串转换为其他数据类型

//用istringstream将字符串转换为数值
template <class T>
inline T fromString(const string &str) {
    istringstream is(str);  //创建字符串输入流
    T v;
    is >> v;    //从字符串输入流中读取变量v
    return v;   //返回变量v
}

int main() {
    int v1 = fromString<int>("5");
    cout << v1 << endl;
    double v2 = fromString<double>("1.2");
    cout << v2 << endl;
    return 0;
}
/*
输出结果:
5
1.2
*/

输入/输出流

两个重要的输入/输出流

  • 一个iostream对象可以是数据的源或目的
  • 两个重要的I/O流类都是从iostream派生的,它们是fstream和stringstream。这些类继承了前面描述的istream和ostream类的功能

fstream类

  • fstream类支持磁盘文件输入和输出
  • 如果需要在同一个程序中从一个特定磁盘文件读并写到该磁盘文件,可以构造一个fstream对象
  • 一个fstream对象是有两个逻辑子流的单个流,两个子流一个用于输入,另一个用于输出

stringstream类

  • stringstream类支持面向字符串的输入和输出
  • 可以用于对同一个字符串的内容交替读写,同样是由两个逻辑子流构成

异常处理

异常接口声明

一个函数显式声明可能抛出的异常,有利于函数的调用者为异常处理做好准备

可以在函数的声明中列出这个函数可能抛掷的所有异常类型

void fun() throw(A,B,C,D);

若无异常接口声明,则此函数可以抛掷任何类型的异常

不抛掷任何类型异常的函数声明如下:

void fun() throw();
#include <iostream>
using namespace std;
int divide(int x, int y) {
    if (y == 0)
        throw x;
    return x / y;
}
int main() {
    try {
        cout << "5 / 2 = " << divide(5, 2) << endl;
        cout << "8 / 0 = " << divide(8, 0) << endl;
        cout << "7 / 1 = " << divide(7, 1) << endl;
    } catch (int e) { //若改成float e就不会catch到
        cout << e << " is divided by zero!" << endl;
    }
    cout << "That is ok." << endl;
    return 0;
}

自动的析构

找到一个匹配的catch异常处理后

  • 初始化异常参数
  • 将从对应的try块开始到异常被抛掷处之间构造(且尚未析构)的所有自动对象进行析构

从最后一个catch处理之后开始恢复执行

#include <iostream>
#include <string>
using namespace std;
class MyException {
public:
    MyException(const string &message) : message(message) {}
    ~MyException() {}
    const string &getMessage() const { return message; }
private:
    string message;
};

class Demo {
public:
    Demo() { cout << "Constructor of Demo" << endl; }
    ~Demo() { cout << "Destructor of Demo" << endl; }
};
void func() throw (MyException) {
    Demo d;
    cout << "Throw MyException in func()" << endl;
    throw MyException("exception thrown by func()");
}

int main() {
    cout << "In main function" << endl;
    try {
        func();
    } catch (MyException& e) {
        cout << "Caught an exception: " << e.getMessage() << endl;
    } 
    cout << "Resume the execution of main()" << endl;
    return 0;
}
/*
运行结果:
In main function
Constructor of Demo
Throw MyException in func()
Destructor of Demo
Caught an exception: exception thrown by func()
Resume the execution of main()
*/
已标记关键词 清除标记
表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页