c++类和结构体的初始化
在C++中,类和结构体的初始化方式有一些不同。下面是它们的一些初始化方式:
类的初始化
- 默认构造函数: 如果类定义了默认构造函数,可以使用没有参数的构造函数进行初始化。
class MyClass {
public:
MyClass() {
// 默认构造函数
}
};
MyClass obj; // 使用默认构造函数初始化
- 带参构造函数: 如果类定义了带参数的构造函数,可以使用相应参数进行初始化。
class MyClass {
public:
MyClass(int x) {
// 带参构造函数
}
};
MyClass obj(42); // 使用带参构造函数初始化
- 成员初始化列表: 在类的构造函数中使用成员初始化列表进行初始化。
class MyClass {
private:
int data;
public:
MyClass(int x) : data(x) {
// 使用成员初始化列表初始化成员变量
}
};
MyClass obj(42); // 使用构造函数初始化
结构体的初始化
- 默认初始化: 结构体可以像类一样通过默认构造函数进行初始化。
struct MyStruct {
int x;
float y;
};
MyStruct obj; // 默认初始化
- 成员初始化: 结构体的成员可以通过成员初始化进行初始化。
struct MyStruct {
int x;
float y;
};
MyStruct obj = {42, 3.14f}; // 成员初始化
- 初始化列表: 可以使用初始化列表来初始化结构体。
struct MyStruct {
int x;
float y;
};
MyStruct obj{42, 3.14f}; // 初始化列表
总的来说,类和结构体的初始化方式有很多相似之处,但在使用成员初始化列表等方面可能存在一些差异。在实际使用中,选择合适的初始化方式取决于类或结构体的定义和使用场景。
在C++中,一旦你创建了类或结构体的对象,你就可以通过对象调用其成员函数(方法)或成员变量。以下是一些示例:
类的使用
class MyClass {
private:
int data;
public:
MyClass(int x) : data(x) {}
// 成员函数
void printData() {
std::cout << "Data: " << data << std::endl;
}
};
int main() {
// 创建对象
MyClass obj(42);
// 调用成员函数
obj.printData();
return 0;
}
结构体的使用
struct MyStruct {
int x;
float y;
// 成员函数
void printValues() {
std::cout << "x: " << x << ", y: " << y << std::endl;
}
};
int main() {
// 创建对象
MyStruct obj = {42, 3.14f};
// 调用成员函数
obj.printValues();
return 0;
}
在这两个示例中,通过对象(obj
)调用了类和结构体中的成员函数。如果成员函数有参数,你可以像调用其他函数一样传递参数。
如果需要访问类或结构体的成员变量,可以使用对象的成员访问运算符 .
来实现。例如,obj.data
或 obj.x
将访问相应的成员变量。但请注意,类的私有成员在类外部不可直接访问。
在上述示例中,对象的创建方式取决于它是在栈上还是在堆上分配的。在 main
函数中,我展示了在栈上创建对象的方式。
栈上创建对象
MyClass obj(42); // 栈上分配对象
MyStruct obj = {42, 3.14f}; // 栈上分配对象
在这种情况下,对象的生命周期与其所在的作用域相同,当超出作用域时,对象将被自动销毁。
堆上创建对象
如果你希望在堆上分配对象,可以使用 new
操作符:
MyClass* obj = new MyClass(42); // 堆上分配对象
MyStruct* obj = new MyStruct{42, 3.14f}; // 堆上分配对象
在这种情况下,你负责管理对象的生命周期,需要在不再需要对象时使用 delete
进行手动销毁,以避免内存泄漏:
delete obj; // 手动销毁堆上的对象
请注意,使用 new
和 delete
需要谨慎,因为手动管理内存可能导致一些问题,例如忘记释放内存或在对象已被销毁后访问它。在现代 C++ 中,推荐使用智能指针(如 std::unique_ptr
或 std::shared_ptr
)来更安全地管理动态分配的对象。
new与重载操作符
重载操作符无法直接禁止使用 new
操作符在堆上创建对象。new
操作符是C++的内置语言特性,它允许在堆上分配内存并调用构造函数创建对象。操作符重载主要用于定义用户自定义类型的行为,但它无法直接影响内置的语言特性。
new
关键字在 C++ 中用于动态分配内存,它会在堆(heap)上分配一块内存,并返回该内存块的地址。这提供了在程序运行时动态创建对象或数组的能力。
然而,new
也需要谨慎使用,因为它分配的内存需要手动释放,否则可能导致内存泄漏。每个 new
应该相应地有一个 delete
,以释放分配的内存。
示例:
int* dynamicInt = new int; // 分配一个 int 的内存块
*dynamicInt = 42; // 在动态分配的内存中存储值
std::cout << *dynamicInt << std::endl; // 输出 42
// 使用完后需要手动释放内存
delete dynamicInt;
然而,更好的做法是使用智能指针(如 std::unique_ptr
或 std::shared_ptr
)或标准库容器,以便更安全地管理动态分配的内存,避免手动的 delete
操作,从而降低内存泄漏的风险。
#include <memory>
std::unique_ptr<int> smartDynamicInt = std::make_unique<int>(42);
std::cout << *smartDynamicInt << std::endl; // 输出 42
// 无需手动释放内存,std::unique_ptr 在作用域结束时会自动释放分配的内存
在现代 C++ 中,尽量避免直接使用 new
和 delete
,而选择使用智能指针和标准库容器,以提高代码的安全性和可维护性。
然而,有一些方法可以间接地限制堆上对象的创建:
- 将析构函数声明为私有: 将类的析构函数声明为私有,这样就不能直接在堆上创建对象,因为无法销毁对象。
class MyClass {
private:
~MyClass() {} // 将析构函数声明为私有
};
这种方法会在编译时引起错误,因为不能直接销毁对象。
- 使用智能指针: 在类中使用
std::shared_ptr
或std::unique_ptr
,并将构造函数设为私有,只允许通过工厂函数或其他方法创建对象。
class MyClass {
private:
MyClass(int value) : value(value) {} // 私有构造函数
int value;
public:
static std::unique_ptr<MyClass> createInstance(int value) {
return std::unique_ptr<MyClass>(new MyClass(value));
}
};
这样,使用者只能通过 createInstance
方法创建对象,而不能直接使用 new
。
虽然这些方法可以增加创建对象的限制,但请注意,它们也可能导致一些不便和代码复杂性。在实际设计中,请确保你真正需要限制堆上对象的创建,以及这是否是你的设计的合理一部分。
默认析构函数
C++ 中如果你没有显式定义析构函数,编译器会自动生成一个默认的析构函数。默认的析构函数的行为如下:
- 自动调用成员对象的析构函数: 如果你的类有成员对象,它们的析构函数会被自动调用。这是递归进行的,即成员对象的成员对象也会依次调用。
- 自动调用基类的析构函数: 如果你的类是派生类,它会自动调用基类的析构函数。同样,这是递归的,即派生类的基类的基类也会被依次调用。
- 释放类对象占用的资源: 如果你的类有分配的堆内存、打开的文件、或者其他类似的资源,这些资源需要在析构函数中释放,以防止内存泄漏或资源泄漏。
- 不需要手动调用基类的析构函数: C++ 编译器会负责调用正确的基类析构函数,你不需要手动调用。
以下是一个示例:
class MyClass {
public:
// 没有显式定义析构函数
// 其他成员和方法...
private:
// 一些资源或成员对象...
};
int main() {
MyClass obj; // 对象会在离开作用域时调用析构函数
// 其他操作...
} // 在这里,obj 的析构函数被自动调用
在这个例子中,MyClass
的默认析构函数会在 obj
离开作用域时被调用,它会释放任何可能存在的资源。
C++ default关键字
在C++中,default
关键字有不同的用法,具体取决于它在什么上下文中使用。以下是两种常见的用法:
- 在类的成员函数声明或定义中:
class MyClass {
public:
// 默认构造函数
MyClass() = default;
// 默认拷贝构造函数
MyClass(const MyClass&) = default;
// 默认析构函数
~MyClass() = default;
// 其他成员函数...
private:
// 成员变量和其他内容...
};
在这种情况下,default
的作用是告诉编译器使用默认实现,即编译器会生成对应的构造函数、拷贝构造函数、析构函数等。如果你显式声明了一个构造函数、拷贝构造函数或析构函数,但是又想使用编译器生成的默认实现,你可以使用 = default
。
- 在类的成员函数外部的特殊用法:
class MyClass {
public:
// 默认构造函数的定义
MyClass() = default;
// 其他成员函数...
private:
// 成员变量和其他内容...
};
// 默认构造函数的定义
MyClass::MyClass() = default;
在这种情况下,= default
告诉编译器使用默认生成的实现。这在特殊情况下,例如将构造函数的定义放在类的声明外部时很有用。
总的来说,default
的作用是指定编译器生成默认实现,以确保类的特殊成员函数(如构造函数、拷贝构造函数、析构函数等)符合默认行为。
lambda表达式
c++20:
// 泛型 lambda,operator() 是一个拥有两个(模板)形参的模板
auto glambda = []<class T>(T a, auto&& b) { return a < b; };
// 泛型 lambda,operator() 是一个拥有一个形参包的模板
auto f = []<typename... Ts>(Ts&&... ts)
{
return foo(std::forward<Ts>(ts)...);
};
折叠表达式
_折叠表达式_的实例化按以下方式展开成表达式 e:
- 一元右折叠 (E 运算符 …) 成为 (E1 运算符 (… 运算符 (EN-1 运算符 EN)))
- 一元左折叠 (… 运算符 E) 成为 (((E1 运算符 E2) 运算符 …) 运算符 EN)
- 二元右折叠 (E 运算符 … 运算符 I) 成为 (E1 运算符 (… 运算符 (EN−1 运算符 (EN 运算符 I))))
- 二元左折叠 (I 运算符 … 运算符 E) 成为 ((((I 运算符 E1) 运算符 E2) 运算符 …) 运算符 EN)
(其中 N 是包展开中的元素数量)
#include <climits>
#include <concepts>
#include <cstdint>
#include <iostream>
#include <type_traits>
#include <utility>
#include <vector>
template<typename... Args>
void printer(Args&&... args)
{
(std::cout << ... << args) << '\n';
// (I << ... << E)
}
template<typename T, typename... Args>
void push_back_vec(std::vector<T>& v, Args&&... args) //v 和 args
{
static_assert((std::is_constructible_v<T, Args&&> && ...));
// 确保每个参数都可以构造为 T 类型,对于每一个右值检查能否转为左值
(v.push_back(std::forward<Args>(args)), ...);
// 一个使用逗号运算符的折叠表达式 ( E , ...)
}
template<class T, std::size_t... dummy_pack>
constexpr T bswap_impl(T i, std::index_sequence<dummy_pack...>)
{
T low_byte_mask = (unsigned char)-1;
T ret{};
([&]
{
(void)dummy_pack;
ret <<= CHAR_BIT;
ret |= i & low_byte_mask;
i >>= CHAR_BIT;
}(), ...);
return ret;
}
constexpr auto bswap(std::unsigned_integral auto i)
{
return bswap_impl(i, std::make_index_sequence<sizeof(i)>{});
}
int main()
{
printer(1, 2, 3, "abc");
std::vector<int> v;
push_back_vec(v, 6, 2, 45, 12);
push_back_vec(v, 1, 2, 9);
for (int i : v)
std::cout << i << ' ';
std::cout << '\n';
static_assert(bswap<std::uint16_t>(0x1234u) == 0x3412u);
static_assert(bswap<std::uint64_t>(0x0123456789abcdefull) == 0xefcdab8967452301ULL);
}
UB
“UB” 是 “Undefined Behavior” 的缩写,中文翻译为 “未定义行为”。在计算机科学和编程领域,当程序执行到一些未定义的行为时,编程语言标准并没有规定具体的行为,这样的情况就被称为 Undefined Behavior。
在 C++ 中,如果程序包含未定义行为,那么编译器和执行环境有权以任何方式处理这些情况,包括但不限于崩溃、产生奇怪的结果、修改变量的值等。这种灵活性给予了编译器在优化和实现上的自由度,但同时也带来了风险。
一些导致 UB 的常见例子包括:
- 空指针解引用:对空指针进行解引用是未定义行为。
int* ptr = nullptr;
int value = *ptr; // Undefined Behavior
- 数组越界访问:访问数组越界是未定义行为。
int arr[5];
int value = arr[10]; // Undefined Behavior
- 使用未初始化的变量:使用未初始化的变量的值是未定义的。
int x;
int y = x; // Undefined Behavior
- 整数溢出:对于有符号整数,发生溢出是未定义行为。
int i = INT_MAX;
i = i + 1; // Undefined Behavior
避免 UB 是编写健壮和可靠程序的关键。编程时应当遵循编程语言的规范,避免依赖于未定义的行为,以确保程序的可移植性和稳定性。
可变参数方法模板
// 递归情况:打印第一个参数,然后递归调用打印剩余参数
template<typename... Args>
void printValues(Args... args) {
(std::cout << ... << args);
}
int main() {
printValues(1, 3.14, "Hello", 'a');
return 0;
}
可变参数右值引用方法模板
完美转发
完美转发(Perfect Forwarding)是指在模板函数中以一种方式传递参数,使得参数的值、左值/右值属性、const 属性等都能被保留。这样的设计允许模板函数在接受参数后将其原样转发到其他函数,而不会失去原始参数的特性。
C++11 引入了右值引用(rvalue references)和 std::forward
来支持完美转发。std::forward
用于在模板函数中将参数完美地转发给其他函数。下面是一个简单的示例:
#include <iostream>
#include <utility>
// 模板函数实现完美转发
template<typename T>
void wrapper(T&& arg) {
// 在这里对参数进行其他操作
std::cout << "Wrapper function: " << arg << std::endl;
// 将参数完美地转发给其他函数
process(std::forward<T>(arg));
}
// 接受右值引用参数的函数
void process(int&& value) {
std::cout << "Process function (rvalue): " << value << std::endl;
}
// 接受左值引用参数的函数
void process(const int& value) {
std::cout << "Process function (lvalue): " << value << std::endl;
}
int main() {
int x = 42;
// 调用 wrapper 函数,完美转发参数
wrapper(x); // lvalue
wrapper(123); // rvalue
return 0;
}
在这个例子中,wrapper
函数接受一个通用引用 T&&
,然后使用 std::forward
将这个参数完美地传递给 process
函数。process
函数有两个版本,一个接受右值引用,另一个接受左值引用。通过完美转发,wrapper
中的参数保留了原始的左值/右值属性,因此能够调用适当版本的 process
函数。
在实际应用中,完美转发通常用于实现泛型代码,使得模板函数能够正确地传递参数给其他函数,而不会失去原始参数的属性。
反例
#include <iostream>
#include <utility>
// 模板函数没有使用完美转发
template<typename T>
void wrapper(T arg) {
// 在这里对参数进行其他操作
std::cout << "Wrapper function: " << arg << std::endl;
// 错误地传递参数给其他函数,丧失了原始参数的属性,不知道数据是左值还是右值
process(arg);
}
// 接受右值引用参数的函数
void process(int&& value) {
std::cout << "Process function (rvalue): " << value << std::endl;
}
// 接受左值引用参数的函数
void process(const int& value) {
std::cout << "Process function (lvalue): " << value << std::endl;
}
int main() {
int x = 42;
// 调用 wrapper 函数,没有使用完美转发
wrapper(x); // lvalue
wrapper(123); // lvalue(错误调用了 process(const int&))
return 0;
}
在 wrapper(123)
这一行中,wrapper
函数的参数 arg
接收了一个右值(123
是字面常量,是右值)。然而,由于 wrapper
函数内部没有使用完美转发,而是直接将参数传递给 process
函数,它会调用 process(const int&)
,即接受左值引用的版本。
这就意味着,在 process
函数中,右值 123
被隐式地转换为一个左值,然后传递给 process(const int&)
。这可能导致不符合预期的行为,因为右值引用通常用于表示可以接受对临时值的引用,而左值引用通常用于表示对具名对象的引用。
如果我们使用完美转发,wrapper
函数将能够正确地将右值引用传递给 process(int&&)
,保持右值属性。
指针类型的调用
#include <iostream>
// 函数接受一个整数指针作为参数,并通过指针修改变量的值
void modifyValue(int* ptr) {
(*ptr)++; // 通过指针递增变量的值
}
int main() {
int number = 5;
std::cout << "Original value: " << number << std::endl;
// 传递变量的地址给 modifyValue 函数
modifyValue(&number);
std::cout << "Modified value: " << number << std::endl;
return 0;
}
在这个例子中,modifyValue 函数接受一个整数指针作为参数,通过指针递增传递的变量的值。在 main 函数中,创建了一个整数变量 number,并将其地址传递给 modifyValue 函数。通过这种方式,modifyValue 函数可以修改 number 的值。
引用类型的调用
#include <iostream>
// 函数接受一个整数引用作为参数,并通过引用修改变量的值
void modifyValue(int& ref) {
ref++; // 通过引用递增变量的值
}
int main() {
int number = 5;
std::cout << "Original value: " << number << std::endl;
// 传递变量的引用给 modifyValue 函数
modifyValue(number);
std::cout << "Modified value: " << number << std::endl;
return 0;
}
在这个例子中,modifyValue 函数接受一个整数引用作为参数,通过引用递增传递的变量的值。在 main 函数中,创建了一个整数变量 number,并将其引用传递给 modifyValue 函数。通过这种方式,modifyValue 函数可以修改 number 的值,因为它直接作用于变量的引用。
- 指针常用于动态内存分配和数组操作。
- 引用常用于函数参数传递,使得函数能够修改传递的参数。
引用
在C++中,引用是一种允许在程序中使用别名的机制。引用提供了对变量的别名,允许通过不同的名称来访问相同的内存位置。引用在声明时使用&
符号。
int main() {
int x = 42;
int& ref = x; // ref 是 x 的引用
std::cout << x << std::endl; // 输出 x 的值,即 42
std::cout << ref << std::endl; // 输出 ref 的值,也是 42
x = 10;
std::cout << ref << std::endl; // 输出 ref 的值,现在是 10
return 0;
}
在上述例子中,ref
是 x
的引用,这意味着它与 x
共享相同的内存位置。因此,对 ref
的修改实际上就是对 x
的修改。在打印 ref
的值时,它输出的是引用所引用变量的值,因此它与 x
的值一致。
引用在许多情况下被用于创建更清晰、更简洁的代码,同时在某些情况下也提供了性能优势。在使用引用时要小心,确保引用不会超出其有效范围。
#include <iostream>
#include <utility>
#include <string>
class TestClass {
public:
const char* p;
int a;
};
void test1(TestClass** cls) {
// new在内部
TestClass* ccls = new TestClass();
ccls->p = "123456";
ccls->a = 6;
*cls = ccls;
}
void test2(TestClass* cls) {
// 在外部创建,cls是一个指针
cls->a = 4;
cls->p = "1234";
}
void test3(TestClass& cls) {
//在外部创建,是一个名称引用,指向相同地址
//是testclass类型而非指针类型
cls.p = "123";
cls.a = 3;
}
int main() {
int x = 42;
TestClass* mainclass;
test1(&mainclass);
// ->应用于指针
std::cout << mainclass->a << mainclass->p;
TestClass mainclass2;
//指针传递方式,不能修改地址,在main的栈上创建
test2(&mainclass2);
std::cout << mainclass2.a << mainclass2.p;
TestClass mainclass3;
//引用传递方式,不能修改地址,在main的栈上创建
test3(mainclass3);
std::cout << mainclass3.a << mainclass3.p;
delete mainclass;
return 0;
}
s