【C++类和数据抽象】类的定义和声明

Source

目录

一、类的基本概念

1.1 什么是类

1.2 类与对象的关系

1.3 类的核心组成

1.3.1 成员变量

1.3.2 初始化方式

二、类的声明

2.1 声明的语法

2.2 声明与定义的区别

三、类的定义

3.1 定义的语法

3.2 类的成员访问控制

四、构造函数和析构函数

4.1 构造函数

4.2 默认构造函数

4.3 析构函数

五、类的友元

5.1 友元函数

5.2 友元类

六、类的静态成员

6.1 静态数据成员

6.2 静态成员函数

七、类的继承与派生

7.1 继承的基本语法

7.2 示例:公有继承 

7.3 继承方式的影响

八、类的多态

8.1 虚函数

8.2 纯虚函数与抽象类

九、类设计最佳实践

9.1 SOLID原则

9.2 异常安全保证

十、常见问题与调试技巧

10.1 典型编译错误

10.2 调试技巧

十一、总结

十二、参考资料


在C++中,类(Class)是面向对象编程(OOP)的核心概念之一,它允许我们将数据(成员变量)和操作数据的方法(成员函数)封装在一起,形成自定义的数据类型。类的定义和声明是掌握C++面向对象编程的基础,本文将全面解析类的定义与声明相关知识。

一、类的基本概念

1.1 什么是类

类是一种用户自定义的数据类型,它可以包含数据成员(变量)和成员函数(方法)。类就像是一个蓝图,它定义了对象的属性和行为。例如,我们可以定义一个Person类来表示一个人,这个类可能包含姓名、年龄等数据成员,以及获取姓名、增加年龄等成员函数。

1.2 类与对象的关系

对象是类的实例。类定义了对象的通用结构和行为,而对象则是根据类的定义创建的具体实体。可以将类看作是模板,而对象是根据这个模板制造出来的产品。例如,根据Person类,我们可以创建出多个具体的Person对象,每个对象都有自己独特的姓名和年龄。

1.3 类的核心组成

1.3.1 成员变量

① 存储类型对比

类型 声明方式 生命周期 内存位置
普通成员 int value; 对象生命周期 对象内存块
静态成员 static int s; 程序生命周期 全局数据区
常量成员 const int c; 对象生命周期 对象内存块
引用成员 int& ref; 对象生命周期 对象内存块

1.3.2 初始化方式

class InitDemo {
public:
    // 直接初始化(C++11)
    int a = 10;
    
    // 构造函数初始化列表
    InitDemo(int b_val) : b(b_val) {}
    
private:
    const int b;
    int& c = a;  // 引用成员必须初始化
};

二、类的声明

2.1 声明的语法

类的声明通常使用class关键字,其基本语法如下:

class ClassName {
    // 成员声明
};

这里的ClassName是类的名称,大括号内是类的成员声明部分。例如,声明一个简单的Point类:

class Point {
    int x;
    int y;
};

Point类包含两个数据成员xy,用于表示二维平面上的一个点的坐标。

2.2 声明与定义的区别

类的声明只是告诉编译器有这样一个类存在,但并没有提供类的具体实现。而类的定义则需要提供类的成员函数的具体代码。例如,可以先声明一个Circle类:

class Circle {
    double radius;
    double getArea();
};

这里只是声明了Circle类有一个数据成员radius和一个成员函数getArea,但getArea函数的具体实现还没有给出。

三、类的定义

3.1 定义的语法

类的定义需要给出成员函数的具体实现。有两种方式可以实现类的成员函数:

① 在类内部定义

可以直接在类的大括号内给出成员函数的定义,例如: 

class Rectangle {
    int width;
    int height;
public:
    int getArea() {
        return width * height;
    }
};

getArea函数的定义直接放在了Rectangle类的内部。

②在类外部定义

也可以在类外部定义成员函数,但需要使用作用域解析运算符::来指定函数所属的类。例如: 

class Square {
    int side;
public:
    int getPerimeter();
};

int Square::getPerimeter() {
    return 4 * side;
}

 getPerimeter函数的定义在类外部,通过Square::来表明它是Square类的成员函数。

3.2 类的成员访问控制

C++ 使用访问修饰符来控制类的成员的访问权限,主要有三种访问修饰符:publicprivateprotected三个关键字实现,决定了类成员的可见性和访问权限。

访问修饰符 类内部 类外部 派生类
public ✔️ ✔️ ✔️
private ✔️
protected ✔️ ✔️

① public

public成员可以被类的外部代码直接访问。例如:

class Student {
public:
    string name;
    void displayName() {
        cout << "Name: " << name << endl;
    }
};

int main() {
    Student s;
    s.name = "Alice";
    s.displayName();
    return 0;
}

namedisplayName都是public成员,所以可以在main函数中直接访问。

private

private成员只能被类的成员函数访问,类的外部代码无法直接访问。例如:

class BankAccount {
private:
    double balance;
public:
    void deposit(double amount) {
        balance += amount;
    }
    double getBalance() {
        return balance;
    }
};

int main() {
    BankAccount account;
    account.deposit(1000);
    cout << "Balance: " << account.getBalance() << endl;
    // account.balance = 2000; // 错误,不能直接访问private成员
    return 0;
}

这里balanceprivate成员,只能通过depositgetBalance等成员函数来访问。

③ protected

protected成员类似于private成员,但可以被派生类的成员函数访问。这在继承关系中非常有用,后续会详细介绍。 

④默认访问权限

  • 类中默认的访问权限是private
  • struct默认的访问权限是public

四、构造函数和析构函数

4.1 构造函数

构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的成员。构造函数的名称与类名相同,并且没有返回类型。例如:

class Book {
    string title;
    int pages;
public:
    Book(string t, int p) {
        title = t;
        pages = p;
    }
    void displayInfo() {
        cout << "Title: " << title << ", Pages: " << pages << endl;
    }
};

int main() {
    Book myBook("C++ Primer", 976);
    myBook.displayInfo();
    return 0;
}

Book类有一个构造函数,它接受两个参数titlepages,并将它们赋值给类的成员变量。

4.2 默认构造函数

如果没有定义任何构造函数,编译器会自动生成一个默认构造函数,将所有成员变量初始化为默认值(如int初始化为0,指针初始化为nullptr)。

①带参数的构造函数

可以定义带参数的构造函数来初始化对象:

class Point {
public:
    double x;
    double y;

    // 带参数的构造函数
    Point(double x_val, double y_val) {
        x = x_val;
        y = y_val;
    }
};

int main() {
    Point p(3.0, 4.0); // 使用带参数的构造函数创建对象
    return 0;
}

②构造函数初始化列表

使用初始化列表可以更高效地初始化成员变量,尤其是const成员和引用成员:

class MathUtils {
public:
    static int add(int a, int b) {
        return a + b;
    }
};

int main() {
    int result = MathUtils::add(3, 5);
    cout << "Result: " << result << endl;
    return 0;
}

4.3 析构函数

析构函数也是一种特殊的成员函数,用于在对象销毁时执行一些清理工作,例如释放动态分配的内存。析构函数的名称是在类名前加上波浪号~,并且没有返回类型和参数。例如: 

class ArrayWrapper {
    int* arr;
    int size;
public:
    ArrayWrapper(int s) {
        size = s;
        arr = new int[size];
    }
    ~ArrayWrapper() {
        delete[] arr;
    }
};

int main() {
    {
        ArrayWrapper wrapper(10);
        // 对象在离开这个代码块时会自动调用析构函数
    }
    return 0;
}

 ArrayWrapper类的析构函数负责释放动态分配的数组内存。

五、类的友元

5.1 友元函数

友元函数是一种可以访问类的私有和保护成员的非成员函数。要将一个函数声明为类的友元函数,需要在类的定义中使用friend关键字。例如:

class Rectangle {
    int width;
    int height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    friend int getArea(const Rectangle& rect);
};

int getArea(const Rectangle& rect) {
    return rect.width * rect.height;
}

int main() {
    Rectangle rect(5, 3);
    cout << "Area: " << getArea(rect) << endl;
    return 0;
}

getArea函数是Rectangle类的友元函数,所以它可以直接访问Rectangle类的私有成员widthheight

5.2 友元类

除了友元函数,还可以将一个类声明为另一个类的友元类。友元类的所有成员函数都可以访问被友元类的私有和保护成员。例如: 

class ClassA {
private:
    int data;
public:
    ClassA(int d) : data(d) {}
    friend class ClassB;
};

class ClassB {
public:
    void displayData(const ClassA& obj) {
        cout << "Data in ClassA: " << obj.data << endl;
    }
};

int main() {
    ClassA a(10);
    ClassB b;
    b.displayData(a);
    return 0;
}

这里ClassBClassA的友元类,所以ClassB的成员函数displayData可以访问ClassA的私有成员data

六、类的静态成员

6.1 静态数据成员

静态数据成员是类的所有对象共享的成员,它不属于任何一个具体的对象。静态数据成员需要在类内部声明,在类外部定义和初始化。例如:

class Counter {
public:
    static int count;
    Counter() {
        count++;
    }
};

int Counter::count = 0;

int main() {
    Counter c1;
    Counter c2;
    cout << "Number of objects: " << Counter::count << endl;
    return 0;
}

countCounter类的静态数据成员,每次创建Counter对象时,count的值就会加 1。

6.2 静态成员函数

静态成员函数也是类的所有对象共享的函数,它只能访问静态数据成员,不能访问非静态成员。静态成员函数可以通过类名直接调用,不需要创建对象。例如: 

class MathUtils {
public:
    static int add(int a, int b) {
        return a + b;
    }
};

int main() {
    int result = MathUtils::add(3, 5);
    cout << "Result: " << result << endl;
    return 0;
}

这里addMathUtils类的静态成员函数,可以直接通过类名调用。

七、类的继承与派生

继承是面向对象编程的重要特性,允许派生类继承基类的成员。

7.1 继承的基本语法

class DerivedClass : access_specifier BaseClass {
    // 派生类新增成员
};
  • access_specifier:继承方式,包括public(公有继承)、protected(保护继承)、private(私有继承)。

7.2 示例:公有继承 

class Shape {
public:
    void setColor(const std::string& color) {
        this->color = color;
    }

    std::string getColor() const {
        return color;
    }

private:
    std::string color;
};

class Circle : public Shape {
public:
    void setRadius(double radius) {
        this->radius = radius;
    }

    double getRadius() const {
        return radius;
    }

private:
    double radius;
};

int main() {
    Circle c;
    c.setColor("Red"); // 允许:公有继承可以访问基类的公有成员
    std::cout << "Color: " << c.getColor() << std::endl; // 输出:Color: Red
    return 0;
}

7.3 继承方式的影响

继承方式 基类public成员 基类protected成员 基类private成员
public继承 public protected 不可访问
protected继承 protected protected 不可访问
private继承 private private 不可访问

八、类的多态

多态是面向对象编程的另一个重要特性,允许不同类的对象对同一消息作出不同的响应。

8.1 虚函数

在基类中使用virtual关键字声明虚函数,派生类可以重写该函数。

class Shape {
public:
    virtual void draw() const {
        std::cout << "Drawing a shape" << std::endl;
    }
};

class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

int main() {
    Shape* shape = new Circle();
    shape->draw(); // 输出:Drawing a circle
    delete shape;
    return 0;
}

8.2 纯虚函数与抽象类

在基类中定义纯虚函数(= 0),使基类成为抽象类,不能实例化。

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

class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

int main() {
    Shape* shape = new Circle();
    shape->draw(); // 输出:Drawing a circle
    delete shape;
    return 0;
}

九、类设计最佳实践

9.1 SOLID原则

原则 说明 示例
单一职责 一个类只做一件事 分离数据存储和格式转换
开闭原则 对扩展开放,对修改关闭 使用策略模式代替修改类
里氏替换 子类必须能替换基类 保持继承关系的语义一致性
接口隔离 多个专用接口优于单个通用接口 分解庞大的接口类
依赖倒置 依赖抽象而非具体实现 使用抽象基类定义接口

9.2 异常安全保证

安全等级 说明
基本保证 对象保持有效状态,无资源泄漏
强保证 操作要么完全成功,要么保持原状态
不抛保证

承诺不抛出任何异常

十、常见问题与调试技巧

10.1 典型编译错误

  • 未定义引用:忘记实现类成员函数

  • 访问权限错误:误用private成员

  • 虚函数不匹配:签名不一致导致隐藏而非重写

  • 对象切片:值传递多态对象

10.2 调试技巧

使用gdb查看对象内存布局:

(gdb) p /x *(ClassType*)obj_ptr

打印虚函数表:

(gdb) info vtbl obj_ptr

 Valgrind检测内存泄漏:

valgrind --leak-check=full ./program

十一、总结

类是 C++ 面向对象编程的核心,掌握类的定义和声明对于编写高质量的 C++ 代码至关重要。通过不断实践和学习,能够更深入地理解和运用类的各种特性,开发出更加复杂和强大的程序。

十二、参考资料

  •  《C++ Primer(第 5 版)》这本书是 C++ 领域的经典之作,对 C++ 的基础语法和高级特性都有深入讲解。
  • 《Effective C++(第 3 版)》书中包含了很多 C++ 编程的实用建议和最佳实践。
  • 《C++ Templates: The Complete Guide(第 2 版)》该书聚焦于 C++ 模板编程,而using声明在模板编程中有着重要应用,如定义模板类型别名等。
  • C++ 官方标准文档:C++ 标准文档是最权威的参考资料,可以查阅最新的 C++ 标准(如 C++11、C++14、C++17、C++20 等)文档。例如,ISO/IEC 14882:2020 是 C++20 标准的文档,可从相关渠道获取其详细内容。
  • :这是一个非常全面的 C++ 在线参考网站,提供了详细的 C++ 语言和标准库文档。
  • :该网站提供了系统的 C++ 教程,配有丰富的示例代码和清晰的解释,适合初学者学习和理解相关知识。
  • 《C++标准库(第2版)》Nicolai M. Josuttis 著

  • Effective STL Scott Meyers 著

  • C++ Core Guidelines:C++ Core Guidelines

  • C++ Reference:https://en.cppreference.com/w/