c++类、结构体、堆、栈及函数调用

c++类和结构体的初始化

在C++中,类和结构体的初始化方式有一些不同。下面是它们的一些初始化方式:

类的初始化

  1. 默认构造函数: 如果类定义了默认构造函数,可以使用没有参数的构造函数进行初始化。
class MyClass {
public:
    MyClass() {
        // 默认构造函数
    }
};

MyClass obj;  // 使用默认构造函数初始化
  1. 带参构造函数: 如果类定义了带参数的构造函数,可以使用相应参数进行初始化。
class MyClass {
public:
    MyClass(int x) {
        // 带参构造函数
    }
};

MyClass obj(42);  // 使用带参构造函数初始化
  1. 成员初始化列表: 在类的构造函数中使用成员初始化列表进行初始化。
class MyClass {
private:
    int data;

public:
    MyClass(int x) : data(x) {
        // 使用成员初始化列表初始化成员变量
    }
};

MyClass obj(42);  // 使用构造函数初始化

结构体的初始化

  1. 默认初始化: 结构体可以像类一样通过默认构造函数进行初始化。
struct MyStruct {
    int x;
    float y;
};

MyStruct obj;  // 默认初始化
  1. 成员初始化: 结构体的成员可以通过成员初始化进行初始化。
struct MyStruct {
    int x;
    float y;
};

MyStruct obj = {42, 3.14f};  // 成员初始化
  1. 初始化列表: 可以使用初始化列表来初始化结构体。
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.dataobj.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;  // 手动销毁堆上的对象

请注意,使用 newdelete 需要谨慎,因为手动管理内存可能导致一些问题,例如忘记释放内存或在对象已被销毁后访问它。在现代 C++ 中,推荐使用智能指针(如 std::unique_ptrstd::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_ptrstd::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++ 中,尽量避免直接使用 newdelete,而选择使用智能指针和标准库容器,以提高代码的安全性和可维护性。

然而,有一些方法可以间接地限制堆上对象的创建:

  1. 将析构函数声明为私有: 将类的析构函数声明为私有,这样就不能直接在堆上创建对象,因为无法销毁对象。
class MyClass {
private:
    ~MyClass() {}  // 将析构函数声明为私有
};

这种方法会在编译时引起错误,因为不能直接销毁对象。

  1. 使用智能指针: 在类中使用 std::shared_ptrstd::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++ 中如果你没有显式定义析构函数,编译器会自动生成一个默认的析构函数。默认的析构函数的行为如下:

  1. 自动调用成员对象的析构函数: 如果你的类有成员对象,它们的析构函数会被自动调用。这是递归进行的,即成员对象的成员对象也会依次调用。
  2. 自动调用基类的析构函数: 如果你的类是派生类,它会自动调用基类的析构函数。同样,这是递归的,即派生类的基类的基类也会被依次调用。
  3. 释放类对象占用的资源: 如果你的类有分配的堆内存、打开的文件、或者其他类似的资源,这些资源需要在析构函数中释放,以防止内存泄漏或资源泄漏。
  4. 不需要手动调用基类的析构函数: C++ 编译器会负责调用正确的基类析构函数,你不需要手动调用。

以下是一个示例:

class MyClass {
public:
    // 没有显式定义析构函数

    // 其他成员和方法...

private:
    // 一些资源或成员对象...
};

int main() {
    MyClass obj;  // 对象会在离开作用域时调用析构函数
    // 其他操作...
}  // 在这里,obj 的析构函数被自动调用

在这个例子中,MyClass 的默认析构函数会在 obj 离开作用域时被调用,它会释放任何可能存在的资源。

C++ default关键字

在C++中,default 关键字有不同的用法,具体取决于它在什么上下文中使用。以下是两种常见的用法:

  1. 在类的成员函数声明或定义中:
class MyClass {
public:
    // 默认构造函数
    MyClass() = default;

    // 默认拷贝构造函数
    MyClass(const MyClass&) = default;

    // 默认析构函数
    ~MyClass() = default;

    // 其他成员函数...

private:
    // 成员变量和其他内容...
};

在这种情况下,default 的作用是告诉编译器使用默认实现,即编译器会生成对应的构造函数、拷贝构造函数、析构函数等。如果你显式声明了一个构造函数、拷贝构造函数或析构函数,但是又想使用编译器生成的默认实现,你可以使用 = default

  1. 在类的成员函数外部的特殊用法:
class MyClass {
public:
    // 默认构造函数的定义
    MyClass() = default;

    // 其他成员函数...

private:
    // 成员变量和其他内容...
};

// 默认构造函数的定义
MyClass::MyClass() = default;

在这种情况下,= default 告诉编译器使用默认生成的实现。这在特殊情况下,例如将构造函数的定义放在类的声明外部时很有用。

总的来说,default 的作用是指定编译器生成默认实现,以确保类的特殊成员函数(如构造函数、拷贝构造函数、析构函数等)符合默认行为。

lambda表达式

CSDN_1705853143588.png
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:

  1. 一元右折叠 (E 运算符 …) 成为 (E1 运算符 (… 运算符 (EN-1 运算符 EN)))
  2. 一元左折叠 (… 运算符 E) 成为 (((E1 运算符 E2) 运算符 …) 运算符 EN)
  3. 二元右折叠 (E 运算符运算符 I) 成为 (E1 运算符 (… 运算符 (EN−1 运算符 (EN 运算符 I))))
  4. 二元左折叠 (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 的常见例子包括:

  1. 空指针解引用:对空指针进行解引用是未定义行为。
int* ptr = nullptr;
int value = *ptr;  // Undefined Behavior
  1. 数组越界访问:访问数组越界是未定义行为。
int arr[5];
int value = arr[10];  // Undefined Behavior
  1. 使用未初始化的变量:使用未初始化的变量的值是未定义的。
int x;
int y = x;  // Undefined Behavior
  1. 整数溢出:对于有符号整数,发生溢出是未定义行为。
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;
}

在上述例子中,refx 的引用,这意味着它与 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
  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值