C++类
一.类定义
定义一个类需要使用关键字 class,然后指定类的名称,并类的主体是包含在一对花括号中,主体包含类的成员变量和成员函数。
定义一个类,本质上是定义一个数据类型的蓝图,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。
以下实例我们使用关键字 class 定义 Box 数据类型,包含了三个成员变量 length、breadth 和 height:class Box
{
public:
double length; // 盒子的长度
double breadth; // 盒子的宽度
double height; // 盒子的高度
};
二.定义对象
在 C++ 中,根据初始化方式的不同,类对象的定义主要分为以下几种。这种分类方式侧重于对象创建时如何获取其初始状态。
1. 默认初始化
定义:在创建对象时,不提供任何初始化参数,编译器会调用类的默认构造函数(Default Constructor)来初始化对象。
语法:类名 对象名;
说明:
- 如果类中没有显式定义任何构造函数,编译器会自动生成一个默认构造函数。
- 如果类中定义了其他构造函数,但没有显式定义默认构造函数,那么编译器将不会自动生成,此时不能使用这种方式创建对象(会编译报错)。
- 对于内置类型(如
int,double),默认初始化的变量值是未定义的(垃圾值)。
示例:class Person {
public:
string name;
int age;
// 编译器会自动生成默认构造函数 Person()
};
int main() {
Person p1; // 默认初始化,调用 Person()
// p1.name 是 std::string 的默认值(空字符串)
// p1.age 的值是不确定的(垃圾值)
return 0;
}
2. 显式初始化 (Explicit Initialization)
定义:在创建对象时,通过括号 () 或花括号 {} 提供初始化参数,编译器会调用对应的带参数构造函数。
语法:类名 对象名(参数1, 参数2, ...); // 圆括号语法 (C++03)
类名 对象名{参数1, 参数2, ...}; // 统一初始化语法 (C++11 起推荐)
说明:
- 这种方式是最常用、最清晰的初始化方式。
- 花括号
{}语法被称为“统一初始化”或“列表初始化”,它可以防止窄化转换(例如,不能将一个double隐式转换成int),是现代 C++ 推荐的风格。
示例:class Person {
public:
string name;
int age;
// 带参数的构造函数
Person(string n, int a) : name(n), age(a) {}
};
int main() {
Person p1("Alice", 30); // 显式初始化,调用 Person(string, int)
Person p2{"Bob", 25}; // 统一初始化语法,效果同上
// Person p3("Charlie"); // 编译错误,没有匹配的构造函数
return 0;
}
3. 拷贝初始化
定义:用一个已存在的对象来初始化一个新对象,编译器会调用类的拷贝构造函数(Copy Constructor)。
语法:类名 对象名 = 已存在的对象;
说明:
- 这里的
=是初始化符号,不是赋值运算符。 - 编译器会创建一个新对象,其状态是对已有对象的复制。
示例:class Person {
public:
string name;
int age;
Person(string n, int a) : name(n), age(a) {}
// 编译器会自动生成拷贝构造函数 Person(const Person& other)
};
int main() {
Person p1("Alice", 30);
Person p2 = p1; // 拷贝初始化,调用拷贝构造函数
// p2 的 name 和 age 都和 p1 相同
return 0;
}
4. 移动初始化
定义:这是 C++11 引入的特性,用于将一个右值(通常是临时对象)的资源“移动”到新对象中,以避免不必要的拷贝。编译器会调用类的移动构造函数。
语法:类名 对象名 = std::move(右值对象);
说明:
std::move()是一个模板函数,它能将一个左值强制转换为右值引用,以触发移动语义。- 移动构造函数通常会“窃取”源对象的内部资源(如动态分配的内存),并将源对象置于一个有效但未指定的状态(通常是清空)。这比拷贝构造函数效率更高。
示例:
class Person {
public:
string name;
int* data; // 假设 data 指向一个动态分配的数组
Person(string n, int size) : name(n) {
data = new int[size];
cout << "构造函数: " << name << endl;
}
// 拷贝构造函数
Person(const Person& other) : name(other.name) {
data = new int[sizeof(other.data)/sizeof(int)];
memcpy(data, other.data, sizeof(other.data));
cout << "拷贝构造函数: " << name << endl;
}
// 移动构造函数
Person(Person&& other) noexcept : name(std::move(other.name)), data(other.data) {
other.data = nullptr; // 源对象的资源被窃取,置为 nullptr
cout << "移动构造函数: " << name << endl;
}
~Person() {
if (data) delete[] data;
cout << "析构函数: " << name << endl;
}
};
Person createPerson() {
return Person("临时对象", 10); // 返回一个临时对象(右值)
}
int main() {
Person p1 = createPerson(); // 移动初始化,调用移动构造函数
Person p2("Alice", 5);
Person p3 = std::move(p2); // 强制将 p2 转为右值,触发移动构造
// 此时 p2 的 data 指针已为 nullptr
return 0;
}
输出可能为:构造函数: 临时对象
移动构造函数: 临时对象
析构函数: 临时对象
构造函数: Alice
移动构造函数: Alice
析构函数:
析构函数: Alice
5. 值初始化
定义:这是一种在 C++03 中引入的初始化方式,它确保对象被初始化为一个“合理”的默认值。
语法:类名 对象名(); // 注意:这在某些语境下可能被解析为函数声明,有歧义
类名 对象名{}; // C++11 起,用花括号的统一初始化语法是首选且无歧义的方式
说明:
- 对于类类型,如果存在默认构造函数,则行为与默认初始化类似。
- 对于内置类型(如
int,double),值初始化会将其设置为零(0,0.0)。 - 对于没有默认构造函数的类,如果其所有成员都有默认值,也可以进行值初始化。
示例:class Person {
public:
string name;
int age;
};
int main() {
Person p1{}; // 值初始化
// p1.name 是空字符串,p1.age 是 0
int x{}; // 值初始化,x = 0
double y{}; // 值初始化,y = 0.0
return 0;
}
三.访问数据成员
类的对象的公共数据成员可以使用直接成员访问运算符 . 来访问。
为了更好地理解这些概念,让我们尝试一下下面的实例:
using namespace std;
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
// 成员函数声明
double get(void);
void set( double len, double bre, double hei );
};
// 成员函数定义
double Box::get(void)
{
return length * breadth * height;
}
void Box::set( double len, double bre, double hei)
{
length = len;
breadth = bre;
height = hei;
}
int main( )
{
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
Box Box3; // 声明 Box3,类型为 Box
double volume = 0.0; // 用于存储体积
// box 1 详述
Box1.height = 5.0;
Box1.length = 6.0;
Box1.breadth = 7.0;
// box 2 详述
Box2.height = 10.0;
Box2.length = 12.0;
Box2.breadth = 13.0;
// box 1 的体积
volume = Box1.height * Box1.length * Box1.breadth;
cout << "Box1 的体积:" << volume <<endl; //输出“Box1 的体积:210”
// box 2 的体积
volume = Box2.height * Box2.length * Box2.breadth;
cout << "Box2 的体积:" << volume <<endl;//输出“Box2 的体积:1560”
// box 3 详述
Box3.set(16.0, 8.0, 12.0);
volume = Box3.get();
cout << "Box3 的体积:" << volume <<endl;//输出“Box3 的体积:1536”
return 0;
}
四.成员函数
1)成员函数的基本定义
成员函数在类内部声明,既可以在类内实现,也可以在类外实现。
1. 类内实现
直接在类的定义中写函数体:class Person {
private:
string name; // 成员变量
public:
// 成员函数(类内实现)
void setName(string n) {
name = n; // 访问私有成员变量
}
void showName() {
cout << "Name: " << name << endl;
}
};
2. 类外实现
先在类内声明函数原型,再在类外通过 类名::函数名 形式实现(需要注意作用域解析符 ::):class Person {
private:
string name;
public:
// 成员函数声明
void setName(string n);
void showName();
};
// 类外实现(必须加 Person:: 指明属于哪个类)
void Person::setName(string n) {
name = n;
}
void Person::showName() {
cout << "Name: " << name << endl;
}
2)成员函数的调用
成员函数必须通过类的对象(或指针/引用)调用,语法为:对象名.成员函数名(参数) 或 指针->成员函数名(参数)
示例:int main() {
Person p; // 创建对象
p.setName("Alice"); // 调用成员函数
p.showName(); // 输出:Name: Alice
Person* ptr = &p; // 指针指向对象
ptr->showName(); // 指针调用,输出同上
return 0;
}
3)成员函数的特殊类型
除了普通成员函数,C++ 还有几种特殊的成员函数:
1. 构造函数
构造函数 是 C++ 类的一个特殊成员函数,它的主要作用是初始化类对象。每当创建一个类的对象时,构造函数都会被自动调用,确保对象在被使用之前处于一个合法、有效的状态。
A. 构造函数的核心特性
| 特性 | 描述 |
|---|---|
| 名称 | 构造函数的名称必须与它所属的类名完全相同。 |
| 返回值 | 构造函数没有返回值,连 void 都不写。 |
| 自动调用 | 在对象创建时(如定义变量、使用 new 动态分配内存时)自动调用。 |
| 可重载 | 一个类可以有多个构造函数,只要它们的参数列表不同(即构造函数重载)。 |
| 不能手动调用 | 构造函数不能像普通成员函数那样被显式地调用。 |
B. 构造函数的种类
C++ 中有几种不同类型的构造函数,用于不同的初始化场景:
A. 默认构造函数
特点: 没有参数的构造函数。
- 用户定义: 如果你没有定义任何构造函数,编译器会为你自动生成一个隐式的默认构造函数。
- 显式定义: 如果你定义了任何带参数的构造函数,编译器就不会再自动生成默认构造函数。如果你还需要一个无参构造函数,必须自己定义。
- C++11 增强: 可以使用
= default让编译器生成默认构造函数:class MyClass {
public:
// 显式默认构造函数(无参数)
MyClass() {
std::cout << "Default constructor called." << std::endl;
// 可以在这里进行初始化,例如:
m_value = 0;
}
// C++11 风格:
// MyClass() = default;
private:
int m_value;
};
MyClass obj1; // 调用默认构造函数
B. 带参数的构造函数
特点: 接受参数,用于在对象创建时提供初始值。class Point {
public:
// 带参数的构造函数
Point(int x, int y) {
m_x = x;
m_y = y;
std::cout << "Parameterized constructor called." << std::endl;
}
private:
int m_x, m_y;
};
Point p1(10, 20); // 调用带参数的构造函数
C.复制构造函数
特点: 接受一个同类对象的引用作为参数,用于通过一个现有对象创建另一个新对象。
- 格式:
ClassName(const ClassName& other)
调用时机:
- 用一个对象初始化另一个对象:
MyClass obj2 = obj1; - 作为函数参数进行值传递时。
- 作为函数返回值返回时。
class Data { |
C. 构造函数初始化列表
初始化列表是构造函数中一个非常重要的部分,用于在函数体执行之前,对成员变量进行初始化。
格式: Constructor(...) : member1(arg1), member2(arg2) { ... }
为什么使用初始化列表?
- 效率更高: 对于内建类型(如
int),初始化列表和函数体内赋值没有区别。但对于类类型的成员变量,初始化列表执行的是初始化(调用构造函数),而函数体内是赋值(先调用默认构造函数,再调用赋值运算符)初始化列表更高效。 - 强制要求: 对于以下两类成员,必须使用初始化列表:
- 常量成员变量 (
const):它们必须在定义时初始化。 - 引用成员变量 (
&):它们必须在定义时绑定到对象。class Demo {
public:
const int ID; // 必须在初始化列表中初始化
int& ref; // 必须在初始化列表中初始化
int value;
// 使用初始化列表
Demo(int id, int& r, int v) : ID(id), ref(r), value(v) {
// 构造函数体内:此时 ID, ref, value 都已经初始化完成
// ID = id; // 错误:不能在这里赋值 const 成员
}
};
- 常量成员变量 (
D.重载构造函数
构造函数重载遵循 C++ 函数重载的规则:函数名称相同,但参数的个数、类型或顺序不同。
使用参数默认值可以减少构造函数的重载数量,但必须小心,不能导致歧义。class Example {
public:
// 假设我们只定义这一个构造函数:
Example(int a = 0, int b = 0) {
// ...
}
// 此时,以下调用都是有效的:
// Example obj1; // 使用 a=0, b=0 (表现为默认构造函数)
// Example obj2(10); // 使用 a=10, b=0
// Example obj3(10, 20); // 使用 a=10, b=20
};
在这种情况下,如果再定义一个无参的 Example() 构造函数,会导致 Example obj1; 调用时产生歧义,编译器将报错。
2. 析构函数
- 用于对象销毁时释放资源(如动态内存),函数名是
~类名,无返回值,无参数。 - 一个类只能有一个析构函数,若未定义,编译器自动生成默认析构函数(空实现)。
示例:class MyArray {
private:
int* data;
public:
MyArray(int size) {
data = new int[size]; // 分配动态内存
}
~MyArray() {
delete[] data; // 释放内存,避免泄漏
}
};
3. 常成员函数(const 成员函数)
- ==在函数声明和实现的末尾加
const,表示该函数不会修改类的成员变量==。 - 常对象(
const 类名 对象)只能调用常成员函数。
示例:class Person {
private:
string name;
public:
void setName(string n) { // 普通函数,可修改成员
name = n;
}
void showName() const { // 常成员函数,不可修改成员
cout << name << endl; // 只读是允许的
// name = "Tom"; // 错误:常函数不能修改成员
}
};
// 使用
const Person p; // 常对象
p.showName(); // 正确:常对象调用常函数
// p.setName("..."); // 错误:常对象不能调用普通函数
四.访问修饰符
- 在
class中,未指定访问修饰符的成员默认是private。 - 在
struct中,未指定访问修饰符的成员默认是public(这是class和struct的主要区别)。
| 修饰符 | 类内部 | 类外部 | 派生类内部 | 主要用途 |
|---|---|---|---|---|
public |
可访问 | 可访问 | 可访问 | 定义对外接口(函数/必要变量) |
private |
可访问 | 不可访问 | 不可访问 | 隐藏内部实现(核心数据/函数) |
protected |
可访问 | 不可访问 | 可访问 | 支持继承中的成员复用 |
五.拷贝构造函数
在 C++ 中,拷贝构造函数(Copy Constructor)是一种特殊的构造函数,用于用已存在的对象初始化新对象(即创建对象的副本)。它是实现对象“复制”功能的核心机制,也是类的特殊成员函数之一。
1)拷贝构造函数的定义
拷贝构造函数的函数名与类名相同,无返回值,且唯一参数是本类对象的引用(通常用 const 修饰,避免意外修改原对象)。
基本语法:class 类名 {
public:
// 拷贝构造函数
类名(const 类名& 已有对象名) {
// 用已有对象的成员初始化新对象的成员
}
};
2)何时会调用拷贝构造函数?
当发生以下场景时,编译器会自动调用拷贝构造函数:
- 用一个已存在的对象初始化新对象(如
类名 新对象 = 已有对象;)。 - 函数参数为类的对象(传值调用时,会复制实参给形参)。
- 函数返回值为类的对象(返回时会复制局部对象到临时对象)。
3)默认拷贝构造函数
如果用户未显式定义拷贝构造函数,编译器会自动生成一个默认拷贝构造函数。
默认拷贝构造函数的行为是浅拷贝(Shallow Copy):即逐个复制对象的成员变量(简单赋值)。
示例:class Person {
public:
string name;
int age;
// 未定义拷贝构造函数,编译器生成默认版本
};
int main() {
Person p1;
p1.name = "Alice";
p1.age = 20;
// 用 p1 初始化 p2,调用默认拷贝构造函数
Person p2 = p1;
cout << p2.name << " " << p2.age; // 输出:Alice 20(拷贝成功)
return 0;
}
4)为什么需要自定义拷贝构造函数?
默认拷贝构造函数的浅拷贝在大多数简单场景(如成员变量为基本类型或 string 等)可以正常工作,但当类中包含动态分配的资源(如指针指向堆内存)时,浅拷贝会导致资源重复释放或悬挂指针等问题。
在函数参数为类的情况下,如果类有个成员变量为指针,由于默认拷贝构造函数采用浅拷贝(仅复制指针的地址,而非指针指向的内存内容),可能导致内存访问冲突或程序崩溃。
问题示例(浅拷贝的隐患):class MyArray {
private:
int* data; // 指针,指向堆内存
int size;
public:
// 普通构造函数:分配堆内存
MyArray(int s) : size(s) {
data = new int[size];
}
// 析构函数:释放堆内存
~MyArray() {
delete[] data; // 释放资源
}
// 未自定义拷贝构造函数,使用默认浅拷贝
};
int main() {
MyArray arr1(5); // 创建对象,分配堆内存
MyArray arr2 = arr1; // 调用默认拷贝构造函数(浅拷贝)
// 此时 arr1.data 和 arr2.data 指向同一块堆内存!
// 程序结束时,arr2 先析构(释放 data),arr1 再析构(再次释放同一块内存 → 崩溃)
return 0;
}
问题原因:浅拷贝仅复制指针的值(地址),导致两个对象的指针指向同一块堆内存。析构时会对同一块内存释放两次,引发程序崩溃。
5)自定义拷贝构造函数(深拷贝)
为解决浅拷贝的问题,需要自定义拷贝构造函数,实现深拷贝(Deep Copy):即不仅复制成员变量的值,还为指针重新分配内存,并复制原内存中的数据。
解决示例(深拷贝):class MyArray {
private:
int* data;
int size;
public:
MyArray(int s) : size(s) {
data = new int[size];
}
// 自定义拷贝构造函数(深拷贝)
MyArray(const MyArray& other) : size(other.size) {
// 为新对象的指针分配独立内存
data = new int[size];
// 复制原对象内存中的数据
for (int i = 0; i < size; i++) {
data[i] = other.data[i];
}
}
~MyArray() {
delete[] data; // 释放各自的内存,无冲突
}
};
int main() {
MyArray arr1(5);
MyArray arr2 = arr1; // 调用自定义拷贝构造函数(深拷贝)
// arr1.data 和 arr2.data 指向不同的堆内存,析构时各自释放,无问题
return 0;
}
六.友元
在 C++ 中,友元(Friend) 是一种打破类封装性的机制,允许特定的外部函数或类访问当前类的私有(private)和保护(protected)成员。友元的核心作用是在保证封装性的前提下,为特定场景提供灵活的访问权限(例如某些紧密关联的类或函数需要协作时)。
1)友元函数
友元函数是定义在类外部的普通函数,但通过类中的 friend 声明,使其获得访问该类私有/保护成员的权限。
声明方式:
在类内部通过 friend 返回值类型 函数名(参数列表); 声明,函数定义可在类内或类外。
示例:
using namespace std;
class Circle {
private:
double radius; // 私有成员
public:
Circle(double r) : radius(r) {}
// 声明外部函数为友元
friend double calculateArea(const Circle& c);
};
// 友元函数定义(类外),可直接访问 Circle 的私有成员 radius
double calculateArea(const Circle& c) {
return 3.14 * c.radius * c.radius; // 合法访问私有成员
}
int main() {
Circle c(5.0);
cout << "面积:" << calculateArea(c) << endl; // 输出:78.5
return 0;
}
说明:
calculateArea是普通函数,但被Circle声明为友元,因此能直接访问Circle的私有成员radius。- 友元函数不属于类的成员函数,调用时无需通过对象/类名(直接调用即可)。
2)友元类
如果类 A 被声明为类 B 的友元,则类 A 的所有成员函数都能访问类 B 的私有/保护成员。
声明方式:
在类 B 内部通过 friend class A; 声明,类 A 即可访问类 B 的私有成员。
示例:
using namespace std;
class BankAccount {
private:
double balance; // 私有成员:账户余额
friend class BankManager; // 声明 BankManager 为友元类
public:
BankAccount(double b) : balance(b) {}
};
class BankManager { // 友元类
public:
// 可访问 BankAccount 的私有成员 balance
void checkBalance(const BankAccount& acc) {
cout << "账户余额:" << acc.balance << endl; // 合法访问
}
// 可修改 BankAccount 的私有成员
void addBalance(BankAccount& acc, double amount) {
acc.balance += amount;
}
};
int main() {
BankAccount acc(1000.0);
BankManager manager;
manager.checkBalance(acc); // 输出:1000
manager.addBalance(acc, 500);
manager.checkBalance(acc); // 输出:1500
return 0;
}
说明:
BankManager是BankAccount的友元类,因此其所有成员函数(checkBalance、addBalance等)都能直接访问BankAccount的私有成员balance。- 友元关系是单向的:
BankManager能访问BankAccount的私有成员,但BankAccount不能访问BankManager的私有成员(除非双向声明)。
3)友元成员函数
更精细的控制:仅让类 A 的某个特定成员函数成为类 B 的友元,而非类 A 的所有成员函数。
声明方式:
- 先声明类 A 的存在(前向声明)。
- 在类 B 中声明类 A 的特定成员函数为友元(需指定类名和函数名)。
- 定义类 A 时实现该成员函数(此时已能访问类 B 的成员)。
示例:
using namespace std;
// 前向声明:告知编译器 ClassB 存在
class ClassB;
class ClassA {
public:
// 声明成员函数(后续会成为 ClassB 的友元)
void printB(ClassB& b);
};
class ClassB {
private:
int secret = 100; // 私有成员
// 仅声明 ClassA 的 printB 函数为友元
friend void ClassA::printB(ClassB& b);
};
// 实现 ClassA 的 printB 函数(此时可访问 ClassB 的私有成员)
void ClassA::printB(ClassB& b) {
cout << "ClassB 的私有成员:" << b.secret << endl; // 合法访问
}
int main() {
ClassA a;
ClassB b;
a.printB(b); // 输出:100
return 0;
}
说明:
- 仅
ClassA的printB函数能访问ClassB的私有成员,ClassA的其他成员函数无此权限,比友元类更灵活。
七.内联函数
在 C++ 中,内联函数(Inline Function) 是一种通过编译器优化,将函数调用直接替换为函数体代码的特殊函数,核心目的是减少函数调用的开销,提升程序运行效率。
类内定义的成员函数,指的是在类的声明内部直接写出函数体的成员函数(而非仅在类内声明、在类外实现)。这种函数会被编译器默认视为内联函数(无需显式加 inline 关键字)。
示例:class Student {
private:
int score;
public:
// 类内定义的成员函数(默认内联)
void setScore(int s) { // 直接在类内写函数体
score = s;
}
int getScore() { // 同样默认内联
return score;
}
};
这里的 setScore 和 getScore 就是类内定义的成员函数,编译器会默认按内联处理——当调用 p.setScore(90) 时,可能直接替换为 p.score = 90,省去函数调用的开销。
八.this指针
在 C++ 中,this 指针是一个特殊的指针,它指向当前对象的实例。
在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。
this是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。
当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为 this 指针。
友元函数没有 this 指针,因为友元不是类的成员,只有成员函数才有 this 指针。
九.类的静态成员
在 C++ 中,类的静态成员(Static Member) 是被 static 关键字修饰的成员(包括成员变量和成员函数),它们属于类本身,而非类的某个具体对象。这意味着静态成员被该类的所有对象共享,且无需创建对象即可访问。
1)静态成员变量
静态成员变量是类的所有对象共享的变量,内存中只存在一份副本,所有对象都访问同一块内存。类的静态成员变量是可以通过类名直接访问的。
1.定义与初始化
- 声明:在类内部用
static声明(需指定类型)。 - 初始化:必须在类外单独初始化(不能在构造函数中初始化),格式为
类型 类名::变量名 = 初始值;。
示例:
using namespace std;
class Student {
public:
static int totalCount; // 静态成员变量:记录学生总数(所有对象共享)
string name;
// 构造函数:每次创建对象时,总数+1
Student(string n) : name(n) {
totalCount++;
}
};
// 静态成员变量必须在类外初始化(此处初始值为0)
int Student::totalCount = 0;
int main() {
Student s1("Alice");
Student s2("Bob");
// 访问静态成员变量:通过类名或对象(推荐用类名)
cout << "学生总数(类名访问):" << Student::totalCount << endl; // 输出 2
cout << "学生总数(对象访问):" << s1.totalCount << endl; // 输出 2(同一份数据)
return 0;
}
特点:
- 共享性:所有对象共享同一份静态变量,一个对象修改后,其他对象访问的是修改后的值。
- 生命周期:从程序启动到结束(早于对象创建,晚于对象销毁)。
- 访问权限:受类的访问修饰符(
public/private/protected)限制(如私有静态成员只能在类内访问)。
2)静态成员函数
静态成员函数是属于类本身的函数,不依赖于具体对象,可直接通过类名调用。
1.定义
- 在类内声明时加
static,类外实现时无需重复加static(但需加类名::)。
示例:class Math {
public:
// 静态成员函数:求两数之和(无需对象,直接调用)
static int add(int a, int b) {
return a + b;
}
// 静态成员函数访问静态成员变量
static int getTotal() {
return total; // 合法:静态函数可访问静态变量
}
private:
static int total; // 私有静态变量
};
// 初始化私有静态变量
int Math::total = 0;
int main() {
// 调用静态成员函数:直接通过类名(无需创建对象)
int sum = Math::add(3, 5);
cout << sum << endl; // 输出 8
return 0;
}
特点:
- 无
this指针:静态成员函数不属于任何对象,因此不能访问非静态成员(非静态成员依赖于具体对象,需要this指针指向)。 - 访问范围:只能访问类的静态成员(静态变量/静态函数),不能访问非静态成员。
- 调用方式:通过
类名::函数名()调用(推荐),或通过对象调用(不推荐,易混淆)。
十. ::的使用
:: 在 C++ 中被称为作用域解析运算符 (Scope Resolution Operator),是一个非常重要的双冒号符号。它用于明确指定标识符(如变量、函数、类或命名空间)所属的作用域。
1. 访问全局作用域变量
当局部变量的名称与全局变量的名称冲突(即发生名称隐藏)时,可以使用 :: 来明确访问全局变量。
|
2. 访问命名空间 (Namespace) 成员
命名空间用于组织代码,避免命名冲突。:: 是访问命名空间内成员的标准方式。namespace MyLibrary {
int version = 1;
void print() {
std::cout << "In MyLibrary" << std::endl;
}
}
int main() {
// 使用 :: 访问命名空间 MyLibrary 中的成员
std::cout << MyLibrary::version << std::endl; // 输出 1
MyLibrary::print(); // 调用命名空间内的函数
// 最常见的用法:访问标准库 std
std::cout << "Hello" << std::endl;
return 0;
}
注意: 当使用 using namespace std; 后,你可以省略 std::,但这在大型项目中不被推荐,因为它可能重新引入命名冲突。
3. 访问类 (Class) 的静态成员
静态成员(变量或函数)属于类本身,而不是类的特定对象。你可以通过类名和 :: 来访问它们。class Logger {
public:
static int logCount; // 静态成员变量
static void incrementCount() { // 静态成员函数
logCount++;
}
};
// 静态成员变量必须在类外定义和初始化
int Logger::logCount = 0;
int main() {
// 使用类名 :: 访问静态成员
Logger::incrementCount();
Logger::incrementCount();
std::cout << "Log count: " << Logger::logCount << std::endl; // 输出 2
return 0;
}
4. 定义类的方法(成员函数)
当你在类定义(通常在头文件 .h 或 .hpp 中)中声明了一个成员函数,而将它的实现(函数体)放在类定义外部(通常在源文件 .cpp 中)时,必须使用 :: 来指明该方法属于哪个类。
// 假设这是 MyClass.h |
在这个场景中,MyClass::printMessage 告诉编译器,正在实现的 printMessage 是 MyClass 这个作用域内部的成员。
5. 访问嵌套类型
:: 用于访问类或命名空间内部定义的嵌套类型(如嵌套类、枚举等)。class Outer {
public:
// 嵌套枚举类型
enum InnerState {
STATE_A,
STATE_B
};
};
int main() {
// 使用 Outer:: 访问内部的枚举类型
Outer::InnerState state = Outer::STATE_A;
// ...
return 0;
}
十一.构造对象的顺序
1.局部和静态对象,以声明的顺序构造
2.静态对象只被构造一次
3.所有全局对象都在主函数main之前被构造
4.全局对象构造时无顺序。对简单应用的单文件程序来说,全局对象按照它们出现的顺序依次进行构造。
5.成员以其在类中的声明的顺序进行构造。
十二.无名对象
无名对象是指通过调用类的构造函数或工厂函数等方式直接在表达式中创建的对象,但没有将其绑定到任何具名变量。
特点:
- 有类型: 它是一个完整的对象,有自己的类型和内存。
- 无名称: 无法通过变量名直接访问。
- 生命周期: 它们的生命周期非常短暂,通常只持续到包含它的完整表达式结束。
示例:class Logger {
public:
Logger(const std::string& msg) {
std::cout << "Logger CONSTRUCTED: " << msg << std::endl;
}
~Logger() {
std::cout << "Logger DESTROYED." << std::endl;
}
};
void process(const Logger& log) {
// ...
}
int main() {
// 这是一个无名对象
Logger("Starting application");
std::cout << "--- Separator ---" << std::endl;
// 另一个无名对象,作为函数参数
process(Logger("Processing task"));
std::cout << "--- End of main ---" << std::endl;
return 0;
}
输出分析:
Logger CONSTRUCTED: Starting application |
十三.临时对象
临时对象是 C++ 编译器在执行某些操作(如类型转换、函数返回值优化等)时自动创建的、不可见的中间对象。所有无名对象都可以看作是临时对象的一种。
临时对象通常在以下情况下产生:
A. 隐式类型转换的结果
当函数需要某种类型
class Wrapper { |
B. 函数返回值的优化(RVO/NRVO)
当函数按值返回一个对象时,理论上会产生一个临时对象来存储返回值。然而,现代编译器通常会使用返回值优化 (RVO) 或命名返回值优化 (NRVO) 来消除这个临时对象,直接在调用者的内存位置构造对象,从而提高效率。
C. 运算符重载的结果
某些运算符重载(如 + 运算符)会返回一个新的对象作为结果,这个结果在没有被立即赋值给具名变量时,就是一个临时对象。
// 假设 Vector 类重载了 + 运算符 |
十四.成员变量的构造和析构顺序
总体的原则是:先构造内部成员,后构造自身;先析构自身,后析构内部成员。
1)构造顺序
构造顺序是严格按照以下三步执行的,发生在外围类构造函数体执行之前:
| 顺序 | 动作 | 决定因素 |
|---|---|---|
| 第 1 步 | 基类构造 | (如果外围类有继承) 按照继承列表中声明的顺序依次构造基类。 |
| 第 2 步 | 成员对象构造 | 严格按照它们在类定义中声明的顺序构造,与它们在初始化列表中的排列顺序无关。 |
| 第 3 步 | 外围类构造 | 执行外围类构造函数花括号 {} 内部的代码。 |
成员变量的构造顺序由它们在类定义中的 声明顺序 决定,与初始化列表中的顺序无关。
示例:class Inner {
public:
Inner(int id) { std::cout << "Inner(" << id << ") constructed." << std::endl; }
~Inner() { std::cout << "Inner destructed." << std::endl; }
};
class Outer {
private:
Inner m_b; // 声明顺序 1
Inner m_a; // 声明顺序 2
public:
// 初始化列表顺序是 m_a, m_b (与声明顺序相反)
Outer() : m_a(1), m_b(2) {
std::cout << "Outer body executing." << std::endl;
}
};
int main() {
Outer obj;
return 0;
}
实际输出结果:
Inner(2) constructed. // 优先构造 m_b (因为 m_b 先声明) |
可以看到,尽管初始化列表写的是 m_a 在前,但实际构造顺序仍是 m_b(先声明)在前。
2)析构顺序
析构顺序与构造顺序完全相反,遵循 “后进先出” (LIFO) 原则,保证对象和它的成员能够安全地清理资源。
| 顺序 | 动作 | 决定因素 |
|---|---|---|
| 第 1 步 | 外围类析构 | 执行外围类析构函数花括号 {} 内部的代码(用于清理自身资源)。 |
| 第 2 步 | 成员对象析构 | 严格按照它们在类定义中声明的 逆序 析构。 |
| 第 3 步 | 基类析构 | 按照继承列表的逆序析构基类。 |
在析构过程中,首先执行外围类自身的析构函数体。这允许外围类在清除其成员之前,先完成自己的清理工作,例如释放由自身管理、但依赖于成员对象的资源。
基于上面的示例,main 函数结束时 obj 对象的析构顺序:
实际输出结果:Outer destructed. // 1. 执行 Outer 自身的析构函数体
Inner destructed. // 2. 析构 m_a (构造顺序是 2)
Inner destructed. // 3. 析构 m_b (构造顺序是 1)
