Skip to content

C++ Class 基础

class Box {
    public:
        // 成员变量
        double length, breadth, height;
        // 成员函数
        double get(void) { return length * breadth * height; }
        void set(double len, double bre, double hei);
};

void Box::set(double len, double bre, double hei) {
    length = len; breadth = bre; height = hei;
}

public 是访问修饰符,用于定义类的成员的访问权限。有三种访问权限:

  • public:外部可访问
  • private:外部不可访问,只有类和友元可访问
  • protected:外部不可访问,只有类和派生类可访问

创建对象的两种方式:

Box box1; // 栈上创建对象
Box* box2_p = new Box; // 堆上创建对象,必须手动通过 delete 释放内存

访问成员变量和成员函数:

box1.length = 1.0;  // 声明为 public 的成员变量可以直接访问
box2_p->set(1.0, 2.0, 3.0);
double volume2 = box2_p->get();

成员变量和成员函数

成员变量和普通变量类似,有包含数据类型和名称。但在类定义时不能对成员变量赋值,因为类只是一种数据类型,并不占用内存空间。成员变量通常以 m_ 开头,这是约定俗成的写法。

类的成员函数和普通函数一样,拥有返回值和参数列表,不同的是,成员函数的作用范围由类而决定,而普通函数作用范围是全局或某一命名空间。成员函数的定义可以写在类体中(如上节的 get 函数),也可放在类体的外面(如上节 set 函数)。在类外定义成员函数,需要在类体中先进行函数声明,而定义时需要在函数名前加上 类名:: 予以限定,:: 称为域解析符,以指明当前函数属于哪个类。

在类体中定义的成员函数会自动成为内联函数,在类体外定义的不会。所以实际开发中,通常会把较长的成员函数 的函数声明放入类体,而定义放在类体外。如果要把类体外部定义的成员函数变为内联函数,可以定义时添加 inline 关键字,这种在类体外定义 inline 函数的方式,必须将类的定义和成员函数的定义都放在同一个文件中。

编译器会将对象的成员变量和成员函数分开存储,每个对象的成员变量分别存储在堆或栈中,而所有对象共享同一份存放在代码段的成员函数。

C++ 函数编译时,会根据它所在的命名空间、所属的类、以及参数列表等信息进行重命名,形成新的函数名,这个过程称为名字编码(Name Mangling)。

成员函数最终会通过名字编码编译成与对象无关的全局函数。为了在对象调用成员函数时能够找到正确的成员变量,编译器会在编译时为成员函数添加一个额外的参数:一个指向对象的指针, 即 this 指针。通过 this 指针成员函数可以找到成员变量的位置。this 指针会在对象创建以后由编译器自动赋值,它是 const 指针,不能被修改。

构造函数

构造函数名字和类名相同,没有返回类型,可以重载。构造函数会在对象创建时自动调用,这意味着它必须是 public 属性的。

class Box {
    public:
        double length;
        double breadth;
        double height;
        Box(double len, double bre, double hei);
};

Box::Box(double len, double bre, double hei) {
    length = len; breadth = bre; height = hei;
}

如果没有声明构造函数,编译器会自动生成一个默认构造函数 Box()。但如果声明了构造函数,编译器就不会生成默认构造函数,从而我们创建对象时,就必须调用我们声明的构造函数。如上面的例子中,我们创建对象时,就必须传入三个参数 Box box1(1.0, 2.0, 3.0);,而不能直接使用 Box box1;,这是因为无参的构造函数并不存在。

构造函数初始化列表

以上面的 Box 类为例,其构造函数可以使用初始化列表来初始化成员变量:

Box::Box(double len, double bre, double hei): length(len), breadth(bre), height(hei) {}

这种写法和上面的写法等价,但是更加简洁。需要注意的是初始化列表中的初始化顺序和成员变量声明的顺序一致,而不是和初始化列表中的顺序一致。

对于初始化列表,更重要的是在以下几种情况下:

  • 初始化 const 成员变量:如果类中有 const 成员变量,必须使用初始化列表来初始化。
  • 初始化成员对象:如果类中有成员对象,必须使用初始化列表来初始化。这是因为,在创建这个类的对象时,它的成员对象也要被创建,编译器需要知道它需要调用的成员对象的构造函数是哪一个,因此必须用初始化列表来初始化成员对象。
    class Point {
        int x, y;
        Point(int x, int y): x(x), y(y) {}
        Point(): x(0), y(0) {}
    };
    class Vector {
        Point begin, end;
        Vector(int x1, int y1, int x2, int y2): begin(x1, y1), end(x2, y2) {}
    };
    

对象数组初始化

class Point {
    public:
        int x, y;
        Point(int x, int y): x(x), y(y) {}
        Point(int y): x(0), y(y) {}
        Point(): x(0), y(0) {}

};
若要创建 Point 类的对象数组,可以使用以下方法:

Point points_1[3] = {Point(1, 2), Point(3, 4), Point(5, 6)};
Point points_2[3] = { {1, 2}, {3, 4}, {5, 6} };
Point points_3[3] = { 1, {3, 4}}; // points_3 三个元素分别调用 Point(int y), Point(int x, int y), Point() 构造函数

析构函数

析构函数的名字是在类名前加一个波浪号 ~,没有参数,没有返回值,不能重载。析构函数会在对象销毁时自动调用。

class Array {
    public:
        int* ptr;
        Array(int len);
        ~Array();
};

Array::Array(int len) {
    ptr = new int[len];
}

Array::~Array() {
    delete[] ptr;
}

在 C++ 中若要在堆上创建对象,可以使用 newdelete,也可以使用 C 中的 malloc()free(),但是最好使用 newdelete,这是因为 newdelete 会自动调用构造函数和析构函数,而 mallocfree 不会。

成员对象和封闭类

一个类的成员变量如果是另一个类的对象,即“成员对象”。包含成员对象的类叫做封闭类(enclosed class)。封闭类的成员对象初始化需要使用封闭类构造函数的初始化列表。

封闭类对象生成时,先执行所有成员对象的构造函数,然后才执行封闭类自己的构造函数,成员对象的构造顺序与成员对象在类定义中的次序一致。封闭类消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数,析构顺序和构造函数的执行顺序相反。

static 成员变量与成员函数

static 成员变量

静态成员变量是类的所有对象共享的成员变量,不属于任何对象,而是属于类,使用 static 关键字修饰。无论创建多少个对象,也只为静态成员变量分配一份内存,所有对象共享这一份内存中的数据。static 成员变量和普通的 static 变量类似,在内存分区中的全局数据区中分配内存。

class Student {
    private:
        char *m_name;
        int m_age;
    public:
        static int m_total;
        Student(char *name, int age): m_name(name), m_age(age) {} 
};

静态成员变量必须在类外初始化,且只能初始化一次。初始化时可以不赋初值,默认初始化为 0。

// type class::name = value;
int Student::m_total = 0;

static 成员变量既可以通过类来访问(class::name),也可以通过对象来访问(obj.nameobj_p->name

static 成员函数

普通成员函数可以访问所有成员,静态成员函数只能访问静态成员。普通成员函数会隐式增加一个形参 this,所以普通成员函数只能在对象创建后通过对象来调用。但静态成员函数不会增加 this 形参,可以通过类来直接调用,但因为没有 this 指针,所以不能访问成员变量。

class Student {
    private:
        char *m_name;
        int m_age;
    public:
        Student(char *name, int age): m_name(name), m_age(age) {} 
        static int m_total;
        static int getTotal();
};

int Student::getTotal() {
    return m_total;
}

与静态成员变量一样,静态成员函数即可以用类来访问,也可以通过对象来访问。

const 成员变量和成员函数

const 成员变量的数据不能被修改,初始化 const 成员变量只能通过构造函数的初始化列表。

const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值。const 成员函数需要在函数声明和定义的时候在函数头部的结尾加上 const 关键字。

class Student {
    private:
        char *m_name;
        int m_age;
    public:
        Student(char *name, int age): m_name(name), m_age(age) {} 
        char *getname() const;
};

char *getname() const { 
    return m_name;
}

需要注意的是

  • const 成员函数的声明和定义结尾都要加上 const 关键字。这是因为 char *getname() constchar *getname() 是两种不同的函数原形。
  • const 成员函数是在函数头的结尾加上 const 关键字,而在函数开头的 const 是修饰返回值类型的。

const 对象

如果一个对象被 const 关键字修饰,则称为常对象。常对象只能调用 const 成员变量和 const 成员函数。

const class_name object(params);
class_name const object(params);
const class_name *p = new class_name(params);
class_name const *p = new class_name(params);

友元函数和友元类

private 属性的成员只能由本类和友元(friend)访问。

友元函数

友元函数可以访问当前类中的所有成员,包括 public、protected、private 属性的。友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。

  1. 将非成员函数声明为友元函数

    class Student {
        private:
            char *m_name;
            int m_age;
        public:
            Student(char *name, int age): m_name(name), m_age(age) {} 
            friend void show(Student *pstu);  
    };
    
    void show(Student *pstu) {
        cout << pstu->m_name << ": " pstu->m_age << endl;
    
  2. 将其他类的成员函数声明为友元函数

    class Address; // 提前声明 Address 类
    class Student {
        private:
            char *m_name;
            int m_age;
        public:
            Student(char *name, int age): m_name(name), m_age(age) {} 
            void show(Address *addr);  // 声明Student的show成员函数
    };
    
    class Address {
        private: 
            char *m_province, *m_city;
        public:
            Address(char *province, char *city): m_province(province), m_city(city) {}
            friend void Student::show(Address *addr); // 声明Student::show 是 Address 的友元函数
    };
    
    void Student::show(Address *addr) {
        cout << m_name << "(age: " << m_age << "addr: " << addr->m_province << "省" << addr->m_city << "市)" << endl;
    }
    

友元类

友元类中的所有成员函数都是另外一个类的友元函数。如将 Student 类声明为 Address 的友元类,只需要在 Address 类的声明中添加 friend class Student; 就行了。此时,Student 类中的成员函数可访问 Address 类的所有成员。

需要注意的是

  • 友元的关系是单向而不是双响的。
  • 友元关系不可传递。

class 和 struct 区别

C++ 中 struct 类似于 class,即可以包含成员变量,也可以包含成员函数。但是与 class 相比,有如下不同:

  1. class 的成员默认是 private 的;struct 的成员默认是 public 的。
  2. class 继承默认是 private 继承;struct 基础默认是 public 继承。
  3. class 可以使用模板;但 struct 不可以。

string 类

string 是 C++ 中常用的一个类。使用它需要包含头文件 <string>

#include <iostream>
#include <string>
using namespace std;

int main() {
    string s1;
    string s2 = "hello world";
    string s3 = s2;
    string s4(5, 'a');
    int len2 = s2.length(); // 获取字符串长度
    return 0;
}
  • 获取字符串长度:s.length()
  • 转换为 const char* 类型字符串:s.c_str()
  • 访问字符串中的字符:s[0]s[1]
  • 字符串拼接:s1 + s2s1s2 既可以都是 string 类型字符串,也可以其中一个是 string 类型,另一个是 C 风格字符串、字符数组 或 单独的字符)
  • 插入:s.insert(pos, s2)(在 s 的下标为 pos 的字符位置插入 s2
  • 删除:s.erase(pos, len)(从 pos 起删除 len 个字符,如果不指定 len,则删到结束)
  • 子串:s.substr(pos, len)
  • 查找:
    • s.find(str, pos)(从 pos 开始查找 str,返回第一次出现的起始下标,如果没找到返回 string::npos
    • s.rfind(str, pos)(如果到 pos 还没找到,返回 string::npos
    • s.find_first_of(str)(查找 sstr 共同具有的字符中,在 str 中首次出现的位置)