类与对象
需要预先学习:
1. 类与对象
前面学习的 int、double 等内置类型只能表示单个数据。当你想描述一个学生——有姓名、学号、成绩,还会做自我介绍——散落的几个变量无法把"数据"和"操作"组织在一起。C++ 为此提供了类(class)机制,让你可以定义自己的类型。
- 类(class):一种用户自定义类型,描述一类事物共有的特征(数据)和行为(操作)。
- 对象(object):类的具体实例(instance),每个对象都拥有自己的一份数据。
比如"人"是一个类——所有人都有名字、年龄,都会打招呼,这些共性是"人"这个类描述的;而张三、李四则是两个具体的对象,他们拥有同样的结构,但名字、年龄各不相同。
类与对象的关系,就像图纸和按图纸造出来的实物:图纸只有一份,不占实际空间;每按图纸造出一个实物,都要占用真实的空间。对应到程序中:类本身不占内存,每实例化一个对象,就会分配一块独立的内存。
2. 类的定义
用 class 关键字定义类:
#include <iostream>
#include <string>
class Person
{
public: // 访问权限限定符,有 public、private、protected 三种
std::string m_name; // 成员变量
int m_age; // 成员变量
void say_hello(); // 成员函数声明
};
void Person::say_hello() // 在类外定义成员函数
{
std::cout << "你好,我是" << m_name << ",今年 " << m_age << " 岁" << std::endl;
}
类体由两类成员组成:
- 成员变量(member variable,也叫属性):描述对象拥有的数据。
- 成员函数(member function,也叫方法):描述对象能做的操作。
注意类定义末尾的分号 ; 不能省略。
注:本文的成员变量统一以
m_开头,在成员函数里一眼就能区分成员变量和局部变量;构造函数参数则用普通名字,不再加_后缀。m_只是众多命名风格中的一种,很多项目也用name_这样的后缀风格。
public 是访问权限限定符,表示这些成员在类外可以直接访问。为了让本节例子简洁,成员统一使用 public;三种限定符的具体含义和用法见 封装。
2.1 类的实例化
用类创建对象的过程叫实例化(instantiation)。类和内置类型一样都是一种类型,所以实例化的写法和定义变量一致:
int main()
{
Person zhangsan; // 用 Person 类实例化出对象 zhangsan
zhangsan.m_name = "张三"; // 用 . 访问成员变量
zhangsan.m_age = 20;
zhangsan.say_hello(); // 用 . 调用成员函数
Person lisi;
lisi.m_name = "李四";
lisi.m_age = 18;
lisi.say_hello();
return 0;
}
编译并运行代码后,看到这样的输出:
你好,我是张三,今年 20 岁
你好,我是李四,今年 18 岁
.是成员访问运算符,用法为对象名.成员名- 每个对象都拥有独立的成员变量:给
lisi.m_age赋值不影响zhangsan.m_age - 成员函数的代码只有一份,被所有对象共享;调用时编译器会把对象的地址传给成员函数,让函数知道在操作哪个对象
2.2 成员变量
根据修饰方式,成员变量可以分为几类。
1. 普通成员变量
每个对象各有一份,互不影响,也就是上面例子中的 m_name、m_age。
2. 静态成员变量(static)
- 属于类本身,被所有对象共享,整个程序里只有一份
- 在类内声明,还必须在类外定义——真正的存储空间在类外分配
- 通常用
类名::变量名访问;通过对象访问到的也是同一份
#include <iostream>
class Counter
{
public:
static int m_count; // 类内声明静态成员变量
};
int Counter::m_count = 0; // 类外定义并初始化
int main()
{
Counter c1, c2;
Counter::m_count = 5; // 通过类名访问
std::cout << c1.m_count << std::endl;
c2.m_count = 10; // 通过对象访问,修改的是同一份
std::cout << Counter::m_count << std::endl;
return 0;
}
编译并运行代码后,看到这样的输出:
5
10
3. 常量成员变量(const)
- 每个对象可以拥有不同的值,但对象创建完成后就不能再修改
- 必须在构造函数初始化列表中初始化(见下文"构造函数与析构函数")
#include <iostream>
#include <string>
class Person
{
public:
const int m_id; // 常量成员变量
std::string m_name;
Person(int id, const std::string& name) : m_id(id), m_name(name) {}
};
int main()
{
Person zhangsan(1001, "张三");
std::cout << "id: " << zhangsan.m_id << ", name: " << zhangsan.m_name << std::endl;
zhangsan.m_id = 1002; // 错误!常量成员变量不能修改
return 0;
}
编译时报错:
error: assignment of read-only member 'Person::m_id'
4. 可变成员变量(mutable)
与 const 成员函数配合使用:const 成员函数承诺不修改成员变量,但 mutable 修饰的成员变量不受限制。具体见下文"成员函数"。
2.3 成员函数
成员函数可以在类内声明、类外定义,也可以直接写在类内。写在类外的定义需要用 类名:: 限定,告诉编译器它属于哪个类:
void Person::say_hello() // Person:: 表示这是 Person 的成员函数
{
std::cout << "你好,我是" << m_name << std::endl;
}
成员函数可以直接访问本对象的成员变量,不需要把对象作为参数传进来——这是成员函数与普通函数最大的区别。
const 成员函数
在参数列表后加 const,声明这个函数不会修改成员变量:
#include <iostream>
#include <string>
class Person
{
public:
std::string m_name;
mutable int m_visit_count = 0; // 可变成员变量
void show() const // const 成员函数
{
m_visit_count++; // mutable 成员变量可以在 const 成员函数中修改
std::cout << m_name << " 被访问了 " << m_visit_count << " 次" << std::endl;
}
};
int main()
{
Person zhangsan{"张三"};
zhangsan.show();
zhangsan.show();
return 0;
}
编译并运行代码后,看到这样的输出:
张三 被访问了 1 次
张三 被访问了 2 次
如果在 show 中试图修改普通成员变量,编译会报错:
void show() const
{
m_name = "李四"; // 试图在 const 成员函数中修改成员变量
}
error: no match for 'operator=' (operand types are 'const std::string' and 'const char [7]')
mutable 常用于缓存、访问计数这类"看起来像修改,但不影响对象逻辑状态"的场景。另外,const 对象只能调用 const 成员函数。
3. 构造函数与析构函数
构造函数(constructor)是一种特殊的成员函数,在对象创建时自动调用,负责把对象初始化到可用的状态:
- 函数名与类名相同
- 没有返回值类型(
void也不写) - 支持重载
析构函数(destructor)在对象销毁时自动调用,负责清理资源(释放内存、关闭文件等):
- 函数名是
~加类名 - 没有参数、没有返回值,因此不能重载
先观察它们的调用时机:
#include <iostream>
class Person
{
public:
Person()
{
std::cout << "Person() 被调用" << std::endl;
}
~Person()
{
std::cout << "~Person() 被调用" << std::endl;
}
};
int main()
{
std::cout << "进入 main" << std::endl;
{
Person p; // 对象创建,调用构造函数
std::cout << "p 还活着" << std::endl;
} // 离开作用域,对象销毁,调用析构函数
std::cout << "离开 main" << std::endl;
return 0;
}
编译并运行代码后,看到这样的输出:
进入 main
Person() 被调用
p 还活着
~Person() 被调用
离开 main
3.1 默认生成的构造函数
如果你没有写任何构造函数,编译器会自动生成一个默认构造函数(无参数、函数体为空),所以 Person p; 能直接编译。但只要你写了任意一个构造函数(无论带不带参数),编译器就不再自动生成默认构造函数:
#include <string>
class Person
{
public:
Person(const std::string& name) : m_name(name) {} // 只写了带参构造函数
std::string m_name;
};
int main()
{
Person p; // 错误!默认构造函数已不存在
return 0;
}
编译时报错:
error: no matching function for call to 'Person::Person()'
C++11 起可以用 = default 显式要求编译器生成默认构造函数:
class Person
{
public:
Person() = default; // 显式要求生成默认构造函数
Person(const std::string& name) : m_name(name) {}
std::string m_name;
};
3.2 不同的初始化方式调用不同的构造函数
给同一个类写多个构造函数(重载),实例化时根据参数自动匹配:
#include <iostream>
#include <string>
class Person
{
public:
Person() : m_name("未知"), m_age(0)
{
std::cout << "调用默认构造函数" << std::endl;
}
Person(const std::string& name, int age) : m_name(name), m_age(age)
{
std::cout << "调用带参构造函数" << std::endl;
}
void show() const
{
std::cout << m_name << ", " << m_age << " 岁" << std::endl;
}
private:
std::string m_name;
int m_age;
};
int main()
{
Person p1; // 调用默认构造函数
Person p2("张三", 20); // 调用带参构造函数
Person p3 = {"李四", 18}; // 初始化列表,同样调用带参构造函数
Person p4 = p2; // 调用编译器生成的拷贝构造函数
p1.show();
p4.show();
return 0;
}
编译并运行代码后,看到这样的输出:
调用默认构造函数
调用带参构造函数
调用带参构造函数
未知, 0 岁
张三, 20 岁
拷贝构造与移动构造也属于构造函数,用已有对象初始化新对象时会调用它们,详见 移动语义与std-move。
3.3 构造函数初始化列表
构造函数签名后冒号开头的部分叫构造函数初始化列表(member initializer list),它在函数体执行之前完成成员变量的初始化:
Person(const std::string& name, int age) : m_name(name), m_age(age) {}
const成员变量、引用成员变量只能在初始化列表中初始化- 成员变量的初始化顺序由它们在类中的声明顺序决定,与初始化列表中的书写顺序无关
- 初始化列表是直接构造;先默认构造再在函数体中赋值,则多了一次默认构造的开销


