在C++编程中,自定义类型比如结构体、类的默认值设置和构造逻辑直接关系到程序的运行稳定性,不少运行时错误都源于自定义类型实例未正确初始化。理解默认值的生成规则和构造技巧,能有效减少这类问题。

C++自定义类型的默认行为
当我们定义一个自定义类型后,如果没有显式定义构造函数,编译器会自动生成一个默认构造函数。这个默认构造函数的行为会根据类型的成员不同而有所区别。
内置类型成员的默认初始化
如果自定义类型包含内置类型成员(比如int、double、指针等),使用编译器生成的默认构造函数创建实例时,这些内置类型成员不会被默认初始化,其值是未定义的,也就是垃圾值。
#include <iostream>
struct Student {
int age;
double score;
};
int main() {
Student s1; // 使用编译器生成的默认构造函数
// 此时s1.age和s1.score的值是未定义的,输出结果不可预期
std::cout << s1.age << std::endl;
std::cout << s1.score << std::endl;
return 0;
}
类类型成员的默认初始化
如果自定义类型的成员是其他类类型,且该类型有默认构造函数,那么编译器生成的默认构造函数会调用这些成员的默认构造函数完成初始化。
#include <iostream>
#include <string>
struct Person {
std::string name; // std::string有默认构造函数,会初始化为空字符串
int id; // 内置类型,未定义初始值
};
int main() {
Person p1;
std::cout << p1.name << std::endl; // 输出空字符串,因为string默认构造初始化了
std::cout << p1.id << std::endl; // 输出未定义的值
return 0;
}
设置自定义类型默认值的常用方法
类内初始值
C++11之后支持在自定义类型的成员声明时直接赋予初始值,也就是类内初始值。当创建实例时,如果没有通过构造函数显式初始化该成员,就会使用这个默认值。
#include <iostream>
struct Product {
int price = 0; // 类内初始值,默认值为0
std::string name = "unknown"; // 类内初始值,默认值为unknown
};
int main() {
Product p1;
std::cout << p1.price << std::endl; // 输出0
std::cout << p1.name << std::endl; // 输出unknown
return 0;
}
自定义默认构造函数
我们可以显式定义默认构造函数,在函数体内为成员赋值,或者在初始化列表中设置默认值,这样创建实例时就会按照我们设定的逻辑初始化。
#include <iostream>
class User {
private:
int level;
std::string role;
public:
// 自定义默认构造函数,初始化列表设置默认值
User() : level(1), role("normal") {}
void printInfo() {
std::cout << "level: " << level << ", role: " << role << std::endl;
}
};
int main() {
User u1;
u1.printInfo(); // 输出level: 1, role: normal
return 0;
}
实用的构造技巧
委托构造函数
C++11引入的委托构造函数可以让一个构造函数调用同一个类的其他构造函数,减少重复代码。比如我们可以定义一个全参数构造函数,然后让默认构造函数委托它完成初始化。
#include <iostream>
class Point {
private:
int x;
int y;
public:
// 全参数构造函数
Point(int xVal, int yVal) : x(xVal), y(yVal) {}
// 委托构造函数,委托给全参数构造函数,默认x和y为0
Point() : Point(0, 0) {}
void print() {
std::cout << "x: " << x << ", y: " << y << std::endl;
}
};
int main() {
Point p1;
p1.print(); // 输出x: 0, y: 0
Point p2(3, 4);
p2.print(); // 输出x: 3, y: 4
return 0;
}
拷贝构造与移动构造的合理定义
当自定义类型需要管理资源(比如动态内存)时,需要合理定义拷贝构造函数和移动构造函数,避免浅拷贝带来的资源重复释放问题,同时提升资源转移的效率。
#include <iostream>
#include <cstring>
class MyString {
private:
char* data;
int length;
public:
// 构造函数
MyString(const char* str = "") {
length = std::strlen(str);
data = new char[length + 1];
std::strcpy(data, str);
}
// 拷贝构造函数,深拷贝
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
}
// 移动构造函数
MyString(MyString&& other) noexcept : data(other.data), length(other.length) {
other.data = nullptr;
other.length = 0;
}
~MyString() {
delete[] data;
}
void print() {
if (data) {
std::cout << data << std::endl;
}
}
};
int main() {
MyString s1("hello");
MyString s2 = s1; // 调用拷贝构造函数
s2.print(); // 输出hello
MyString s3 = std::move(s1); // 调用移动构造函数
s3.print(); // 输出hello
return 0;
}
explicit关键字避免隐式转换
如果构造函数只有一个参数,编译器可能会进行隐式转换,有时候这会带来不符合预期的结果。使用explicit关键字修饰构造函数,可以禁止这种隐式转换,让代码更清晰安全。
#include <iostream>
class Number {
private:
int val;
public:
// 使用explicit修饰,禁止隐式转换
explicit Number(int v) : val(v) {}
int getVal() const { return val; }
};
void printNumber(const Number& n) {
std::cout << n.getVal() << std::endl;
}
int main() {
Number n1(10); // 正确,显式构造
printNumber(n1); // 正确
// printNumber(10); // 错误,无法隐式转换int为Number
return 0;
}
注意事项
在使用自定义类型时,要注意区分内置类型和类类型的初始化规则,优先使用类内初始值或者显式构造函数设置默认值,避免依赖未定义的初始值。另外,当自定义类型涉及资源管理时,一定要遵循三五法则,合理定义构造、拷贝、移动、析构相关函数,防止资源泄漏或者重复释放的问题。