1. 构造函数和析构函数
• 构造函数:与类同名,无返回值。对象创建时自动调用,用于初始化成员变量。
• 析构函数:以~类名
命名,无参数无返回值。对象销毁时自动调用,用于资源释放。
示例:
class Demo {
public:
Demo() { std::cout << "无参构造" << std::endl; }
Demo(int a, double b) { std::cout << "有参构造" << std::endl; }
Demo(const Demo& other) { std::cout << "拷贝构造" << std::endl; }
Demo(Demo&& other) { std::cout << "移动构造" << std::endl; }
~Demo() { std::cout << "析构" << std::endl; }
};
2. 构造成员函数的分类及调用
类型 | 说明 | 示例语法 |
---|---|---|
无参构造函数 | 没有参数或所有参数都有默认值 | Demo(); |
有参构造函数 | 带有一个或多个参数 | Demo(int a, double b); |
拷贝构造函数 | 用同类型对象初始化新对象,参数为const 类名& | Demo(const Demo& other); |
移动构造函数 | 用右值对象初始化新对象,参数为类名&&(C++11及以后) | Demo(Demo&& other); |
默认构造函数 | 编译器自动生成的无参构造函数 | Demo();(未自定义时自动生成) |
无参构造函数:
Demo d1; // 直接定义
Demo d2(); // 注意:这是函数声明,不是对象定义
Demo d3{}; // C++11列表初始化
Demo* p = new Demo; // 动态分配
有参构造函数:
Demo d4(10, 3.14); // 直接初始化
Demo d5 = Demo(20, 2.71); // 显式构造
Demo d6{30, 1.23}; // C++11列表初始化
Demo* p2 = new Demo(40, 0.99);
拷贝构造函数:
Demo d7 = d4; // 拷贝初始化
Demo d8(d4); // 直接初始化
Demo* p3 = new Demo(d4);
移动构造函数:
Demo d9 = std::move(d4); // 移动初始化
Demo d10(std::move(d5));
注意事项:
• 如果自定义了有参构造函数,编译器不再自动生成无参构造函数,需手动补充。
• 拷贝构造函数常用于对象按值传递、返回时。
• 移动构造函数用于高效转移资源(如动态内存、容器等)。
• 构造函数可以重载,参数列表不同即可。
3. 拷贝构造函数调用时机
拷贝构造函数用于用一个对象初始化另一个同类型对象
,常见调用时机:
• 用已存在对象初始化新对象:Demo d2 = d1;
• 函数参数按值传递对象
• 函数返回对象(按值返回)
示例:
class Demo {
public:
Demo(const Demo& other) { /* ... */ }
};
4. 构造函数调用规则
• 若未定义任何构造函数,编译器自动生成无参构造、拷贝构造、析构函数。
• 一旦自定义了构造函数,编译器不再自动生成无参构造。
• 构造函数可重载。
5. 深拷贝与浅拷贝
• 浅拷贝:成员变量按值复制,指针成员只复制地址,易导致多次释放同一内存。
• 深拷贝:指针成员分配新内存并复制内容,避免资源冲突。
class Demo {
char* data;
public:
Demo(const Demo& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
~Demo() { delete[] data; }
};
6. 初始化列表
• 构造函数可以使用初始化列表对成员变量进行初始化,语法为: 成员1(值1), 成员2(值2)。
• 成员变量在对象创建时就被初始化,而不是先默认构造再赋值,避免了多次构造和赋值,提高效率。
• 对于内置类型,初始化列表和构造函数体赋值效果类似,但对于类类型,初始化列表可以直接调用目标构造函数。
示例:
class Demo {
int a;
public:
Demo(int x) : a(x) {} // 直接初始化a
// Demo(int x) { a = x; } // 先默认初始化a,再赋值,效率低
};
• const成员和引用成员必须在对象创建时初始化,不能在构造函数体内赋值,否则编译报错,初始化列表是唯一的初始化方式。
示例:
class Demo {
const int a;
int& ref;
public:
Demo(int x, int& y) : a(x), ref(y) {} // 正确
// Demo(int x, int& y) { a = x; ref = y; } // 错误,编译不通过
};
• 如果类成员对象没有默认构造函数,必须通过初始化列表显式调用其有参构造函数,否则编译失败。
class A {
public:
A(int) {}
};
class B {
A a;
public:
B(int x) : a(x) {} // 正确
// B(int x) { a = A(x); } // 错误,a没有默认构造函数
};
7. 类对象作为类成员
在C++中,一个类的成员变量可以是另一个类的对象,这种成员称为成员对象。这种设计常用于“整体-部分”
关系(如:汽车包含发动机对象)。
构造和析构顺序:
• 构造顺序:先调用成员对象
的构造函数,再调用自身的构造函数(成员对象按声明顺序初始化)。
• 析构顺序:先调用自身
的析构函数,再调用成员对象的析构函数(析构顺序与构造相反)。
示例:
#include <iostream>
class Engine {
public:
Engine() { std::cout << "Engine 构造" << std::endl; }
~Engine() { std::cout << "Engine 析构" << std::endl; }
};
class Car {
private:
Engine engine; // 成员对象
public:
Car() { std::cout << "Car 构造" << std::endl; }
~Car() { std::cout << "Car 析构" << std::endl; }
};
int main() {
Car car;
return 0;
}
输出顺序:
Engine 构造
Car 构造
Car 析构
Engine 析构
初始化成员对象:
• 如果成员对象没有默认构造函数,必须在外部类的初始化列表中显式初始化。
示例:
class A {
public:
A(int) {}
};
class B {
A a;
public:
B(int x) : a(x) {} // 必须用初始化列表
};
8. 静态成员
静态成员(static member)包括静态成员变量
和静态成员函数
。它们属于类本身,而不是某个具体对象,所有对象共享同一份静态成员
。
静态成员变量:
• 用static关键字修饰,属于类级别,所有对象共享。
• 必须在类外进行一次性定义和初始化
。
• 可以通过类名或对象访问。
示例:
class Demo {
public:
static int count; // 声明
Demo() { ++count; }
};
int Demo::count = 0; // 类外定义和初始化
int main() {
Demo a, b;
std::cout << Demo::count << std::endl; // 输出2
return 0;
}
静态成员函数:
• 用static修饰,不依赖具体对象。
• 只能访问静态成员变量
和其他静态成员函数
,不能访问非静态成员。
• 可以通过类名或对象调用。
示例:
class Demo {
public:
static int count;
static void showCount() {
std::cout << count << std::endl;
// 不能访问非静态成员
}
};
int Demo::count = 0;
int main() {
Demo::showCount();
return 0;
}
总结:
• 静态成员变量在程序生命周期内只有一份,节省内存。
• 静态成员函数没有this指针。
• 常用于计数、全局配置、工具函数等场景。
9. 成员变量和成员函数的存储
成员变量的存储:
• 成员变量(非静态)存储在每个对象的内存空间中。
• 每个对象都有自己独立的一份成员变量
,互不影响。
• 对象的大小只与成员变量
有关(不包括成员函数)。
示例:
class Demo {
int a;
double b;
};
Demo d1, d2;
// d1和d2各自有一份a和b
// sizeof(Demo) == sizeof(int) + sizeof(double)
成员函数的存储:
• 成员函数的代码只在内存中存一份,属于类本身
,不属于任何对象。
• 所有对象共享同一份成员函数代码。
• 成员函数不占用对象的内存空间。
示例:
class Demo {
int a;
public:
void func() { /* ... */ }
};
Demo d1, d2;
// d1和d2都可以调用func(),但func()代码只存一份
// sizeof(Demo) 只计算a的大小
静态成员变量:
• 静态成员变量也不存储在对象内存中,而是存储在全局数据区
,所有对象共享
。
总结:
• 成员变量:每个对象一份,存储在对象内存中。
• 成员函数:所有对象共享一份,存储在代码区,不占用对象空间。
• 静态成员变量:所有对象共享,存储在全局区,不占用对象空间。
10. this指针
this
指针是每个非静态成员函数
内部隐含的一个指针,指向当前调用该成员函数的对象本身。
基本作用:
• this的类型为ClassName* const,即指向当前对象的常量指针。
• 只能在非静态成员函数中使用,静态成员函数没有this指针。
主要用途:
- 区分成员变量和参数同名的情况:
class Demo {
int a;
public:
void setA(int a) {
this->a = a; // 左边为成员变量,右边为参数
}
};
- 实现链式调用:
class Demo {
int a;
public:
Demo& setA(int a) {
this->a = a;
return *this; // 返回当前对象的引用
}
};
// 用法:Demo d; d.setA(10).setA(20);
- 在成员函数中返回当前对象的指针或引用:
class Demo {
public:
Demo* getThis() {
return this;
}
};
注意事项:
• this指针指向调用成员函数的对象本身。
• 不能在静态成员函数中使用this。
• 通过this可以访问对象的所有成员。
总结:
this指针是C++对象模型的重要组成部分,主要用于区分同名、链式调用和返回自身等场景,提升代码的可读性和灵活性。
11. 空指针访问成员函数
可以通过空指针(nullptr)调用成员函数,但有以下注意事项:
允许的情况:
• 只要成员函数没有访问任何成员变量,通过空指针调用是安全的。
• 这是因为成员函数本质上是类的普通函数,只是多了一个隐式的this指针参数。
示例:
class Demo {
public:
void foo() { std::cout << "ok" << std::endl; }
};
int main() {
Demo* p = nullptr;
p->foo(); // 合法,只要foo未访问成员变量
return 0;
}
危险的情况:
• 如果成员函数访问了成员变量,通过空指针调用会导致未定义行为(通常是程序崩溃)。
示例:
class Demo {
int a = 10;
public:
void bar() { std::cout << a << std::endl; }
};
int main() {
Demo* p = nullptr;
p->bar(); // 非法,访问成员变量a会崩溃
return 0;
}
原理说明:
• 调用成员函数时,编译器会将this指针传递给函数。
• 如果对象指针为nullptr,this就是nullptr。
• 只要成员函数内部不解引用this(即不访问成员变量/函数),就不会出错。
总结:
空指针可以调用成员函数,但前提是该函数不访问任何成员变量或成员函数。否则会导致未定义行为,实际开发中应尽量避免这种用法。
12. const修饰成员函数
const修饰成员函数表示该函数不会修改对象的成员变量(除mutable修饰的成员外),保证对象的常量性和接口的安全性。
基本语法:
class Demo {
int a;
public:
int getA() const { // const成员函数
// a = 10; // 错误,不能修改成员变量
return a;
}
};
• const关键字写在成员函数参数列表后、函数体前。
作用:
• 保证成员函数不会修改对象状态,可用于const对象。
• 只能调用其他const成员函数,不能调用非const成员函数。
使用场景:
• 访问器(getter)等只读操作应声明为const成员函数。
• 使得对象可以作为const引用或const指针安全传递和使用。
示例:
void printDemo(const Demo& d) {
std::cout << d.getA() << std::endl; // 只能调用const成员函数
}
this指针类型的变化:
• 普通成员函数中,this指针的类型是:ClassName* const this
• 在const成员函数中,this指针的类型变为:const ClassName* const this
• 也就是说,const成员函数内部,this指向的对象被视为常量,不能通过this修改成员变量(除mutable成员外)
编译器检查:
• 如果在const成员函数中修改成员变量(非mutable),编译器会报错,增强代码安全性。
总结:
const修饰成员函数可提升代码的健壮性和可读性,是C++类设计的良好实践。