从C到C++:从结构体到类,面向对象初体验

学C的时候,结构体struct就是把一堆变量打包在一起,方便管理。到了C++,class直接把变量和函数都包进去了,还加了封装、继承、多态…整个编程的思路都变了。
这篇就聊聊从C的结构体过渡到C++的类时,那些让我"哦原来如此"的瞬间。
一、从 struct 到 class:不只是加了函数
C的结构体:数据的打包
C里的struct只能放变量,不能放函数(函数指针不算):
// C风格的"学生"结构体
struct Student {
char name[20];
int age;
float score;
};
// 操作结构体的函数是分开的
void student_init(struct Student *s, const char *name, int age, float score) {
strcpy(s->name, name);
s->age = age;
s->score = score;
}

void student_print(struct Student *s) {
printf(“%s, %d岁, 分数: %.1f\n”, s->name, s->age, s->score);
}
数据和操作数据的函数是分开的,调用的时候要把结构体指针传进去。
C++的类:数据 + 操作打包在一起
C++里,struct和class都能定义类(区别只是默认访问权限),可以把数据和函数都放进去:
// C++风格的"学生"类
class Student {
public:
// 成员函数
void init(const string& name, int age, float score) {
name_ = name;
age_ = age;
score_ = score;
}
void print() {
cout << name_ << “, " << age_ << “岁, 分数: " << score_ << endl;
}
private:
// 成员变量
string name_;
int age_;
float score_;
};
用的时候:
Student s;
s.init(“张三”, 18, 95.5f);
s.print();
最大的区别:C里是"数据 + 操作数据的函数”,C++里是"对象自己带着数据和方法”。
就像:C里你是"命令机器去操作数据",C++里你是"让对象自己干活"。
二、三大访问限定符:封装的艺术
C的结构体成员默认都是公开的,想怎么改就怎么改,很自由也很危险。
C++有三个访问限定符,控制成员的可见性:
表格
限定符 作用
public 公开的,谁都能访问
private 私有的,只有类自己的成员函数能访问
protected 受保护的,自己和子类能访问(继承的时候再讲)
默认访问权限:
class 默认是 private
struct 默认是 public(为了兼容C)
class A {
int x; // 默认private
};

struct B {
int x; // 默认public
};

为什么要有封装?
刚开始学的时候觉得"好麻烦,设成public直接改不行吗?"
后来才明白封装的好处:
保护数据:不让外部随便改,防止非法值
解耦:外部只需要知道"怎么用",不需要知道"内部怎么实现"
可维护:内部实现改了,只要接口不变,外部代码不用改
举个例子:
// 不好的设计:成员变量全公开
class Rectangle {
public:
int width;
int height;
};

// 外部可以随便改,甚至改成负数
Rectangle r;
r.width = -10; // 合法但不合理,宽不能是负数啊
// 好的设计:封装起来,只暴露接口
class Rectangle {
public:
bool set_width(int w) {
if (w <= 0) return false; // 检查合法性
width_ = w;
return true;
}

int get_width() const { return width_; } int area() const { return width_ * height_; }

private:
int width_;
int height_;
};

心得:封装不是为了"限制",而是为了"安全"。把该藏的藏起来,把该露的露出去,代码才好维护。
三、构造函数与析构函数:对象的"生"与"死"
C里结构体初始化得自己写个init函数,还容易忘调用。C++有构造函数和析构函数,对象创建和销毁的时候自动调用。
构造函数
对象创建的时候自动调用,用来初始化:
class Student {
public:
// 构造函数:和类同名,没有返回值
Student(const string& name, int age, float score) {
name_ = name;
age_ = age;
score_ = score;
}

void print() const { cout << name_ << ", " << age_ << "岁" << endl; }

private:
string name_;
int age_;
float score_;
};

// 使用
Student s(“张三”, 18, 95.5f); // 创建对象的时候自动调用构造函数
s.print();
构造函数的特点:
函数名和类名一样
没有返回值(连void都不写)
对象创建时自动调用,不用手动调用
可以重载(多个构造函数,参数不同)
默认构造函数
没有参数的构造函数叫默认构造函数:
class Student {
public:
Student() { // 默认构造函数
name_ = “未命名”;
age_ = 0;
score_ = 0;
}

Student(const string& name, int age, float score) { // ... }

};
Student s1; // 调用默认构造函数
Student s2(“张三”, 18, 95.5f); // 调用有参构造

注意:如果你自己没写任何构造函数,编译器会自动生成一个默认构造函数(什么也不做)。但只要你写了一个构造函数,编译器就不会自动生成默认的了。
析构函数
对象销毁的时候自动调用,用来清理资源:
class Array {
public:
Array(int size) {
data_ = new int[size]; // 构造的时候分配内存
size_ = size;
}

~Array() { // 析构函数:~ + 类名,没有参数,没有返回值 delete[] data_; // 析构的时候释放内存 }

private:
int *data_;
int size_;
};
析构函数的特点:
名字是 ~类名
没有参数,没有返回值
对象销毁时自动调用
不能重载,一个类只有一个析构函数
重要的思想:RAII
构造函数获取资源,析构函数释放资源。对象的生命周期绑定资源的生命周期。
这是C++里非常重要的编程思想,智能指针、锁、文件句柄…都是这么设计的。
四、this 指针:我是谁?
成员函数里,怎么知道自己在操作哪个对象的数据?答案是:this 指针。
this 是一个隐含的指针,指向调用这个成员函数的对象:
class Student {
public:
void set_age(int age) {
// age_ = age; 其实等价于下面这句
this->age_ = age; // this指向调用这个函数的对象
}

Student& set_name(const string& name) { name_ = name; return *this; // 返回对象自身的引用,可以链式调用 }

private:
string name_;
int age_;
};
链式调用的例子:
Student s;
s.set_name(“张三”).set_age(18); // 连续调用,因为set_name返回了*this
小知识:静态成员函数没有this指针,因为它不属于某个具体的对象。
五、从C到C++的思维转变

  1. 从"过程式"到"面向对象"
    C(过程式) :我要做什么 → 第一步干什么 → 第二步干什么 → 数据在步骤间传来传去
    C++(面向对象) :有哪些对象 → 每个对象有什么属性和能力 → 对象之间怎么交互
    举个例子:做一个学生管理系统
    过程式思路:先想有哪些功能(增删改查),数据用数组存,函数操作数组
    面向对象思路:先想有哪些类(学生类、管理类),每个类有什么方法
  2. 从"直接操作数据"到"通过接口操作"
    C里数据是裸露的,想怎么改怎么改。C++里数据藏起来,通过接口访问,更安全也更可控。
  3. 从"手动管理资源"到"对象管理资源"
    C里malloc/free全靠人记得。C++里对象构造的时候拿资源,析构的时候放资源,只要对象销毁了,资源就自动释放了(RAII)。
    六、我的踩坑记录
    坑1:构造函数忘了写,结果创建对象报错
    class Student {
    public:
    Student(const string& name) { // 自己写了有参构造,编译器就不生成默认的了
    name_ = name;
    }
    private:
    string name_;
    };
    Student s; // 错误!没有默认构造函数
    解决:要么加一个默认构造函数,要么创建对象的时候传参数。
    坑2:对象里有指针,忘了写析构函数
    class Array {
    public:
    Array(int size) : data(new int[size]) {}
    // 没写析构函数!内存泄漏!
    private:
    int *data;
    };

类里有动态分配的资源,一定要写析构函数释放。还要考虑拷贝构造和赋值运算符(深拷贝浅拷贝的坑,后面讲)。
坑3:const对象不能调用非const成员函数
class Student {
public:
void print() { // 没加const
cout << name_ << endl;
}
private:
string name_;
};

const Student s;
s.print(); // 错误!const对象不能调用非const成员函数

正确写法:不修改成员变量的函数,都要加const:
void print() const { // 加const,表示这个函数不会修改成员变量
cout << name_ << endl;
}
建议:成员函数只要不修改成员变量,就加上const。既是约束自己,也是告诉调用者"这个函数不会改东西"。
坑4:头文件里写类,成员函数的实现忘了加类名
// 在cpp文件里
void print() { // 错误!这是全局函数,不是类的成员函数
cout << name_ << endl;
}

// 正确写法
void Student::print() const { // 要加 Student::
cout << name_ << endl;
}
七、一些实用建议

  1. 成员变量私有化,提供接口访问
    不要把成员变量设成public,用get/set方法访问。既安全又灵活。
  2. 不修改成员的函数加const
    const成员函数可以被const对象调用,也能防止你不小心写错。
  3. 构造函数用初始化列表
    // 写法1:函数体内赋值(不好)
    Student::Student(const string& name, int age) {
    name_ = name;
    age_ = age;
    }

// 写法2:初始化列表(更好)
Student::Student(const string& name, int age)
: name_(name), age_(age) {
// 函数体
}
初始化列表效率更高(直接构造,不是先构造再赋值),而且有些情况必须用初始化列表(比如const成员、引用成员、没有默认构造的成员对象)。
4. 多思考"这个类应该提供什么接口"
写类的时候,先想"外部要怎么用这个类",再想"内部怎么实现"。接口设计好了,内部怎么改都不怕。
写在最后
从结构体到类,不只是加了成员函数那么简单,而是整个编程思维的转变——从"我要一步步怎么做"变成"有哪些对象,它们各自能做什么"。
刚开始学面向对象的时候可能觉得"这有啥用,我用C也能写出来"。但等你写过比较大的项目之后,就会发现面向对象的优势:代码更清晰、更容易维护、更容易扩展。
当然,面向对象也不是银弹,不是什么都要塞到类里。C++是多范式语言——过程式、面向对象、泛型编程…选最合适的就好。