2024-05-30【C/C++】一些语法细节

1 关于n++/-- --/++n

n++先使用n的值,再让n+1
++n先n+1,再使用+1后的值

2 关于累加

  • 一般写法
    sum = sum + delta
  • 简便写法
    sum += delta

3 释放strdup申请的内存 C++申请/释放内存

  • C
 char *duplicate;
    duplicate = strdup(out_folder.c_str());
    strcpy(outfile, duplicate);   //不安全,没释放strdup分配的内存  可定义中间变量释放
    free(duplicate);  //duplicate没有用malloc分配内存  但其同地址指向空间由malloc分配  所有可以用free释放
  • C++
unsigned char *ucBuffer = new unsigned char[ulCount];
//处理
delete[] ucBuffer;

4 容器

4.1 容器begin & end

begin() 指向第一个元素 和end() 指向最后一元素的下一位置 返回迭代器,本质是指针,需用*解引用访问元素值

4.2 容器初始化

  • 规定尺寸 每个元素都是 char 类型的默认值,通常是0或空字符('\0')
std::vector<char> buffer_(1024);
  • 规定元素值 容器内只有单个元素且值为
std::vector<char> buffer_{1024};
  • 其他初始化
  1. 字符串
char[5] = {}  (全0)  或者 char[5] = ""; (全终止符\0)
  1. double 数组
double a[5] = {};  全0

4.3 清空容器

buffer.clear() 容器内元素被移除,其size()为0,但分配的内存空间不变

4.4 删除容器内指定范围内元素

std::vector<int> vec = {1, 2, 3, 4, 5};
// 删除单个元素
vec.erase(vec.begin() + 2);  // 删除索引为2的元素,即元素3
// 删除范围内的元素
vec.erase(vec.begin() + 1, vec.end() - 1);  // 删除索引1到索引3的元素,即元素2和4

相应size()也会减小

4.5 遍历容器

for (auto i = data_buffer_.begin(); i != data_buffer_.end(); i++)

5 C++方法访问限制

不声明保护类型struct 默认public class 默认protected

6 C++多线程

  1. 不需要返回值
  • 头文件 #include <thread>
// 创建多个线程,分别执行不同的函数
    std::thread t1(function1, 其他参数);
    std::thread t2(function2);
    std::thread t3(function3);
    // 等待所有线程执行完毕
    t1.join();
    t2.join();
    t3.join();
    std::cout << "所有函数已经完成执行。" << std::endl;

//将类的方法加入线程
boost::asio::io_service io_service;
std::string port = "COM8"; // 或 "COM1" 在 Windows 上
//1 创建线程 读取串口输入数据
ReadIMUFromSerial imu_receiver(io_service, port);
//2 创建一个工作对象来保持 io_service 活跃
boost::asio::io_service::work work(io_service);
std::thread readImu_thread(&ReadIMUFromSerial::startAsyncRead, &imu_receiver, 方法的参数); //如果传递参数为引用类型,需使用std::ref保护
  1. 需要返回值
  • 头文件#include <thread> <future>
// 使用std::async启动一个异步任务,并返回std::future对象
    std::future<int> resultFuture = std::async(threadFunction, 其他参数);

    // 在主线程中,我们可以使用get()来获取线程的返回值
    int result = resultFuture.get();

    std::cout << "线程返回的值是: " << result << std::endl;

在上面的代码中,std::async用来启动一个异步任务,它返回一个std::future对象,该对象包含了线程函数的返回值。当线程执行完毕后,我们可以通过调用std::future对象的get()方法来获取结果。请注意,如果线程还没有完成,调用get()会阻塞主线程直到线程完成

  1. 将类的实例成员函数加入线程
// 创建类的实例
    SerialReadReceiver read_receiver(/* 参数 */);

    // 创建一个线程来运行成员函数
    std::thread read_thread(&SerialReadReceiver::readData, &read_receiver /*, 成员函数的其他参数 */);

    // 等待线程完成
    read_thread.join();

特别注意
如果传入参数是引用,需使用std::ref(被保护的引用参数)保护引用参数,否则只是值传递

7 变量访问控制锁

#include <mutex>

std::mutex data_mutex; // 全局互斥锁

void safe_increment(int& value) {
    std::unique_lock<std::mutex> lock(data_mutex); // 加锁
    ++value; // 在这里,value 的访问是受保护的
    // lock.unlock(); // 解锁,不过通常不需要手动调用,因为 unique_lock 会在离开作用域时自动解锁
}

std::unique_lock<std::mutex>std::mutex 一起工作,以提供对共享数据的线程安全访问。当你创建一个 std::unique_lock<std::mutex> 对象时,它会尝试锁定与之关联的 std::mutex。如果 std::mutex已经被另一个线程锁定,当前线程将阻塞,直到它能够获得锁。
总结:给自己加锁A,如果锁A被其他线程占用,阻塞当前线程,直到获取锁

8 多线程协同机制

引入头文件
#include <condition_variable>
创建同步条件
std::condition_variable data_cond_var;//线程同步

  • 线程A
    //进入线程A 执行操作
    //操作完毕 唤醒线程B
    data_cond_var.notify_one();// 通知等待的线程数据已经准备好

  • 线程B

std::unique_lock<std::mutex>  lock(data_mutex);
data_cond_var.wait(lock, [&data_process] { return !data_process.empty(); });

如果lambda条件不满足,或未被A唤醒, 线程B都将被阻塞

8.1 更多线程的访问控制

好的,C++ 中的线程管理和数据访问控制可以通过以下几种方式实现:

  1. 使用互斥锁(mutex)保护共享容器的访问
  2. 使用条件变量(condition_variable)协调线程之间的通信

下面是一个示例代码,展示了如何在C++中实现你描述的功能:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>

std::mutex mtx_a, mtx_b, mtx_c;
std::condition_variable cv_a, cv_b;

std::queue<int> a;
std::queue<int> b;
std::queue<int> c;

bool done_a = false;
bool done_b = false;

void A() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx_a);
        a.push(i);
        std::cout << "A pushed: " << i << std::endl;
        cv_a.notify_one();  // Notify one waiting thread
    }
    std::unique_lock<std::mutex> lock(mtx_a);
    done_a = true;
    cv_a.notify_all();  // Notify all waiting threads
}

void B() {
    while (true) {
        std::unique_lock<std::mutex> lock_a(mtx_a);
        cv_a.wait(lock_a, [] { return !a.empty() || done_a; });
        if (a.empty() && done_a) break;

        int data = a.front();
        a.pop();
        lock_a.unlock();

        // Process data
        int processed_data = data * 2;

        std::unique_lock<std::mutex> lock_b(mtx_b);
        b.push(processed_data);
        std::cout << "B processed and pushed: " << processed_data << std::endl;
        cv_b.notify_one();
    }
    std::unique_lock<std::mutex> lock_b(mtx_b);
    done_b = true;
    cv_b.notify_all();
}

void C() {
    while (true) {
        std::unique_lock<std::mutex> lock_b(mtx_b);
        cv_b.wait(lock_b, [] { return !b.empty() || done_b; });
        if (b.empty() && done_b) break;

        int data = b.front();
        b.pop();
        lock_b.unlock();

        // Further process data
        int further_processed_data = data + 1;

        std::unique_lock<std::mutex> lock_c(mtx_c);
        c.push(further_processed_data);
        std::cout << "C further processed and pushed: " << further_processed_data << std::endl;
    }
}

int main() {
    std::thread tA(A);
    std::thread tB(B);
    std::thread tC(C);

    tA.join();
    tB.join();
    tC.join();

    std::cout << "Final contents of c: ";
    while (!c.empty()) {
        std::cout << c.front() << " ";
        c.pop();
    }
    std::cout << std::endl;

    return 0;
}

代码解释:

  1. 共享数据结构

    • 使用std::queue<int>来表示共享容器abc
    • 使用互斥锁std::mutex来保护对共享容器的访问。
    • 使用条件变量std::condition_variable来通知其他线程数据状态的改变。
  2. 线程A

    • 向共享容器a中添加数据。
    • 每次添加数据后使用cv_a.notify_one()通知可能在等待数据的线程B。
  3. 线程B

    • 从共享容器a中读取数据,进行预处理后将数据存入共享容器b
    • 每次添加数据后使用cv_b.notify_one()通知可能在等待数据的线程C。
  4. 线程C

    • 从共享容器b中读取数据,进行进一步处理后将数据存入共享容器c
  5. 主函数

    • 启动三个线程,并在所有线程完成后输出最终容器c中的内容。

这种设计确保了各个线程安全地访问共享数据,并通过条件变量实现了线程之间的同步。

9 打印不同数据类型为10进制

  1. unsigned int
    %u

10 打印不同数据类型为指定进制

  • 十六进制
    %x
  • 八进制
    %o

11 lambda 表达式的捕获

Lambda 表达式本质上是一个匿名函数对象,它可以捕获作用域中的变量,并且可以像普通函数一样被调用。当你使用 lambda 表达式时,编译器会自动为这个表达式生成一个类,这个类重载了 operator(),使得这个 lambda 表达式成为一个可调用对象。

  • 一般用法
serial_port_.async_read_some(boost::asio::buffer(buffer_),
            [this, &data_process](const boost::system::error_code& error, size_t bytes_transferred) {  //[捕获参数](lamda函数的输入传递参数){执行函数}
                handleRead(error, bytes_transferred, data_process);
  • 在类中捕获成员变量 捕获this就好了
data_cond_var.wait(lock, [this] { return !imuRaw.empty(); });
  • 详细解释
    在 C++ 中,lambda 表达式的捕获列表 [] 用于指定 lambda 表达式中可以访问的外部变量。捕获列表中可以包含不同的捕获方式:
  1. [] - 不捕获任何外部变量。
  2. [=] - 以值捕获所有外部变量(按值传递)。
  3. [&] - 以引用捕获所有外部变量(按引用传递)。
  4. [this] - 以值捕获当前对象的指针 this
  5. [var] - 以值捕获变量 var
  6. [&var] - 以引用捕获变量 var
  7. [=, &var] - 以值捕获所有变量,但 var 以引用捕获。
  8. [&, var] - 以引用捕获所有变量,但 var 以值捕获。

在 C++ 中,lambda 表达式的参数列表与普通函数的参数列表类似。它用来声明 lambda 表达式的参数。参数列表放在捕获列表 [] 和主体 {} 之间。

auto lambda = [](参数列表) -> 返回类型 {  // lambda 表达式体 };

例如:

#include <iostream>

int main() {
    // 一个简单的 lambda 表达式,带有两个整数参数
    auto add = [](int x, int y) -> int {
        return x + y;
    };

    int result = add(3, 4);
    std::cout << "Result: " << result << std::endl;  // 输出 7

    return 0;
}

这个 lambda 表达式有两个整数参数 xy,返回它们的和。

12 Eigen使用方法

  • 创建矩阵
注意创建元素的数据类型
double: 
Eigen::Matrix<double, 3, 3> double3D3;
或者
Eigen::MatrixXd mat(3, 3);
  • 分割矩阵
mat.block(起始行索引、起始列索引、要提取的行数和列数);
  • 给特定区块赋值
mat.block(起始行索引、起始列索引、要提取的行数和列数) = 赋值矩阵
或者
mat.block<要提取行数,要提取列数>(起始行索引,起始列索引) = 赋值矩阵
  • rows()和row()
    rows() 返回矩阵行数
    row() mat.row(1); // 返回矩阵的第二行(索引从 0 开始
  • 对每个元素取三角函数
    在 Eigen 中,你可以使用 .array() 方法将矩阵转换为数组表达式,然后对每个元素应用 sin 函数。例如:
Eigen::Matrix3d A; // 创建一个 3x1 的矩阵
// 假设 A 已经被赋值
Eigen::Matrix3d B = A.array().sin(); // 对 A 中的每个元素取正弦值

这里,B 将是一个新的 3x1 矩阵,其元素是 A 中对应元素的正弦值。
Eigen中,凡是设计到对所有单个元素的计算,都需要使用array()转换成队列,如所有元素取平方,Matrix.array().square()

  • 取对角元素
    M是3*3矩阵
Eigen::MatrixXd D = M.diagonal().array().sqrt();
先取对角元素,再对每个元素开平方
  • 行/列逆序重排
    行:ztracemat(1, "行逆序重排\n", intmat.colwise().reverse());
    列:ztracemat(1, "行逆序重排\n", intmat.rowwise().reverse());
  • 对已创建矩阵重新赋值
已有矩阵
Eigen::MatrixXd M(3, 1);
    M << 1, 2, 3;

1.可以使用block()方法取块重新赋值
2.可以使用<< 填充元素 重新填充,元素数量务必等于M容量,否则导致错误

  • 标量【 乘 除】Eigen矩阵
乘:
a * mat;
除:
a * mat.cwiseInverse();
  • 对应元素相除
// 对应元素相除
Eigen::MatrixXd C = A.array() / B.array();
  • 重新设置矩阵维度
mat.resize(rows, clos);

如果维度和设置前不同,会丢失所有元素值

  • 索引规则
    matlab
    有矩阵:
1 2 3
4 5 6
7 8 9

索引 a(3) = 7; 列优先规则,1开始
Eigen
有矩阵:

1 2 3
4 5 6
7 8 9

索引 a(3) = 2; 列优先规则, 0开始

13 创建指向结构体的指针

在C++中,创建一个指向结构体的指针可以通过多种方式完成。对于你提到的 DD5 结构体,以下是一些创建指针的方法:

  1. 动态分配内存:

    DD5* ptr = new DD5;
    
  2. 静态分配内存:

    DD5 obj;
    DD5* ptr = &obj;
    
  3. 使用智能指针(如果你使用C++11或更高版本):

    std::unique_ptr<DD5> ptr = std::make_unique<DD5>();
    // 或者
    std::shared_ptr<DD5> ptr = std::make_shared<DD5>();
    

第一种方法会在堆上分配内存,你需要在不再使用时手动删除它,使用 delete ptr;。第二种方法会在栈上创建一个对象,并将指针指向它,无需手动管理内存。第三种方法使用智能指针,它会自动管理内存,当智能指针超出作用域时,它会自动释放分配的内存。

根据你的需要选择合适的方法。如果你在处理多线程或异步I/O,智能指针可能是一个更安全的选择,因为它们可以减少内存泄漏的风险。

14 结构内使用其他结构体的初始化问题

struct A
{
    //构造器
    A(int x, int y)
    {
        a1 = x;
        a2 = y;
    }
    //成员变量
    int a1;
    int a2;
};
struct B
{
    //构造器
    B(int x, int y) : a(x, y)
    {
        b1 = x;
        b2 = y;
    }
    // B(int x, int y)
    // {
    //     a = A(x, y);  这是赋值操作,而不是初始化,初始化应在赋值操作之前,故而编译错误
    //     b1 = x;
    //     b2 = y;
    // }
    //成员变量
    int b1;
    int b2;
    A a;
};

务必使用初始化列表对不含默认构造器的结构体变量进行初始化
因为执行代码时,会优先初始化成员变量,再执行构造函数

#include <iostream>
#include "struct.h"

int main() {
    B b(1, 2);
    A a(3, 4);
    std::cout << "这是A:\n";
    std::cout << a.a1 << " " << a.a2 << std::endl;

    std::cout << "这是B:\n";
    std::cout << b.a.a1 << " " << b.a.a2 << std::endl;

    std::cout << "测试在初始化后使用构造器改变b中a的值\n";
    b.a = A(5, 6);
    std::cout << b.a.a1 << " " << b.a.a2 << std::endl;

    return 0;
}

在后续使用中,可以使用 b.a = A(5, 6);更新成员的值

15 将函数作为参数传递

  • 函数指针
函数定义:
void func(int a)
{
  操作
}
将函数作为指针传递
void foo(void (*func)(int), int j) {
    func(j);
}
使用foo:
int x=0;
foo(func(x));
  • 使用std::function
    使用 std::function 处理具有多个输入参数的函数,并且能够传递不同类型的可调用对象。
    假设我们有一个函数,它接收两个 int 参数并返回它们的和:
#include <iostream>
#include <functional>

// 定义一个函数,接收两个 int 参数并返回它们的和
int add(int a, int b) {
    return a + b;
}

// 接受 std::function 作为参数的函数
void execute(std::function<int(int, int)> func, int x, int y) {
    int result = func(x, y); // 调用传入的函数并传递参数
    std::cout << "Result: " << result << std::endl;
}

int main() {
    // 传递函数
    execute(add, 3, 5);

    // 传递 lambda 表达式
    execute([](int a, int b) { return a * b; }, 3, 5);

    // 传递函数对象
    struct Multiply {
        int operator()(int a, int b) const {
            return a * b;
        }
    };
    execute(Multiply(), 3, 5);

    return 0;
}

16 左值引用和右值引用

  • 左值引用(对原始对象的别名,二者指向同一内存地址)
    左值引用用于引用左值(lvalue)。左值是指在内存中有确定存储地址的值,可以取地址运算符(&)。通常,左值是表达式的持久值,例如变量、数组元素等。左值引用的声明语法是在类型后加上 &
int x = 10;      // x 是一个左值
int& ref = x;    // ref 是一个左值引用,引用了左值 x

ref = 20;        // 修改 ref 相当于修改 x
std::cout << x;  // 输出 20
  • 右值引用(对原始对象资源的接管,原始对象变为有效但未定义状态)
    右值引用用于引用右值(rvalue)。右值是指在内存中没有确定存储地址的值,通常是临时对象或字面值(如常量、表达式的结果等)。右值引用的声明语法是在类型后加上&&。右值引用主要用于实现转移语义和完美转发,从而提高程序的性能。
int&& rref = 10;  // rref 是一个右值引用,引用了右值 10

rref = 20;        // 可以修改右值引用的值
std::cout << rref;  // 输出 20
  • 转移语义和std::move
    右值引用的一个重要用途是实现转移语义。在标准库中,std::move函数用于将左值转换为右值引用,从而启用转移语义。转移语义允许我们在不进行深拷贝的情况下,将资源从一个对象转移到另一个对象,大大提高了性能。
#include <iostream>
#include <utility>
#include <vector>

int main() {
    std::vector<int> v1 = {1, 2, 3};
    std::vector<int> v2 = std::move(v1);  // 使用 std::move 将 v1 转换为右值引用

    std::cout << "v1 size: " << v1.size() << std::endl;  // 输出 0,v1 的资源被转移到 v2
    std::cout << "v2 size: " << v2.size() << std::endl;  // 输出 3,v2 获得了 v1 的资源

    return 0;
}

在这个例子中,std::movev1转换为右值引用,允许 v2接管v1的资源,而不是进行深拷贝。转移语义提高了效率,尤其是在处理大量数据时。
特别注意

当你使用 std::move 将一个对象的资源转移到另一个对象时,被转移的对象(源对象)的资源会被移动到目标对象中。此时,源对象的资源会变为空或未定义状态。因此,对目标对象的修改不会影响源对象。
例如:

  1. 初始化 v1{1, 2, 3}
  2. 使用 std::vector<int> v2 = std::move(v1)v1 的资源转移到 v2
  3. 此时,v1 的资源已经被转移,通常变为空(size 为 0)。
  4. 修改 v2 的内容后,v1 的内容不受影响,因为 v1 的资源已经被转移了。
    因此,对 v2 的修改不会同步反映到 v1 上。

17 可变参数列表

  • 函数模板
template<typename... Args>
  • 函数传参
作为函数参数声明:bool ztrace(const int level, const std::string& format, Args... args);
作为函数参数输入:snprintf(buffer, sizeof(buffer), format.c_str(), args...);  //格式化写入
作为模板输入:std::bind(std::forward<F>(f), std::forward<Args>(args)...);  //绑定函数

18 函数模板

template<class F, class... Args>
void enqueue(F &&f, Args &&... args) 

在 C++ 中,模板参数 class F 用于表示任何可以调用的类型,包括函数指针、函数对象以及 lambda 表达式。在 template<class F, class... Args> 中,F 是一个模板类型参数,它可以匹配任何类型的可调用对象,包括 lambda 表达式。

19 函数的默认参数设置

如果在h文件中的函数声明中已有默认输入参数,如:

void zquit(int signal_sum = 0);  //退出程序

那对应cpp文件的定义中就不能有默认参数设置,会导致warning

void zquit(int signal_sum = 0)
{}
改为
void zquit()
{}

20 给数组名取地址得到的是什么?

在C和C++中,给数组名取地址会得到不同的结果,具体如下:

  1. 数组名本身

    • 数组名(如arr)在大多数情况下会自动转换为指向数组第一个元素的指针(即&arr[0])。例如:
      int arr[10];
      int* p = arr; // 等价于 int* p = &arr[0];
      
  2. 数组名取地址

    • 给数组名取地址(如&arr)会得到一个指向整个数组的指针。这个指针的类型是int (*)[10],表示指向一个包含10个int元素的数组。例如:
      int arr[10];
      int (*p)[10] = &arr; // p 是一个指向包含10个int元素的数组的指针
      
  3. 区别

    • arr&arr在数值上是相同的,都是数组的起始地址,但它们的类型不同。arr的类型是int*,而&arr的类型是int (*)[10]

21 cpp以静态库形式引用.c中定义的函数

C++会对所有的函数名做修饰以支持重载,重载后的函数名与.c中定义函数名不同,会出现未定义错误

对.c对应.h文件添加

#ifdef __cplusplus
extern "C" {
#endif

#include "v.h"
// 这里是一些 C 语言的声明
void myCFunction(int param);

#ifdef __cplusplus
}
#endif

取消对.c中函数名的修饰

22 引用的正确使用方式

数据类型 + & + 数据类型名 
例如:
int& a;
int*& a;

23 智能指针

使用 std::shared_ptr 定义的指针在其引用计数变为零时会自动删除所管理的对象,因此通常不需要手动删除它们。不过,如果你想提前释放 std::shared_ptr 所管理的对象,可以使用 reset() 方法。以下是一些常见的方法:

1. 自动删除

std::shared_ptr 的引用计数变为零时,所管理的对象会自动被删除。例如:

{
    std::shared_ptr<int> ptr = std::make_shared<int>(42);
    // 当 ptr 超出作用域时,所管理的对象会被自动删除
}

2. 使用 reset() 方法

你可以使用 reset() 方法来提前释放 std::shared_ptr 所管理的对象:

std::shared_ptr<int> ptr = std::make_shared<int>(42);
ptr.reset();  // 释放所管理的对象,并将 ptr 置为空

3. 自定义删除器

如果你需要自定义删除行为,可以在创建 std::shared_ptr 时指定一个自定义删除器。例如:

std::shared_ptr<int> ptr(new int[10], [](int* p) {
    delete[] p;  // 自定义删除器,用于删除数组
});
ptr.reset();  // 调用自定义删除器,释放数组内存

示例代码

#include <iostream>
#include <memory>

void customDeleter(int* p) {
    std::cout << "Deleting pointer with custom deleter" << std::endl;
    delete[] p;
}

int main() {
    std::shared_ptr<int> ptr(new int[10], customDeleter);
    ptr.reset();  // 调用自定义删除器,释放数组内存
    return 0;
}

24 虚方法和多态

定义类时,将类区分为基类和派生类,在基类中定义一些对象的基本方法和成员,同时使用虚方法修饰某些方法,表示这些方法可被派生类【覆盖】【继承】【继承+修改】

  • 覆盖:使用override重写
  • 继承:派生类不用写任何声明、实现,可直接使用基类方法(该方法不为纯虚方法)
  • 继承+修改:AnimalBase::eat(); + 其他派生类独有方法
    如有基类 【AnimalBase.h】
#ifndef ANIMALBASE_H
#define ANIMALBASE_H
#include <iostream>
#include <string>
class AnimalBase
{
public:
    AnimalBase(const std::string& inputName);
    std::string getName() const;
    virtual void eat();  //=0就是纯虚函数,不能实例化,只能被派生类重写
    virtual ~AnimalBase();  // 声明虚析构函数
    // 使用虚析构函数的原因是确保当删除派生类对象时,会先调用派生类的析构函数,然后再调用基类的析构函数。
    // 如果析构函数不是虚函数,那么通过基类指针删除派生类对象时,只会调用基类的析构函数,可能导致资源泄漏。
private:
    std::string name;
};

#endif /* ANIMALBASE_H */

【AnimalBase.cpp】的实现

#include "AnimalBase.h"

AnimalBase::AnimalBase(const std::string& inputName){
    name = inputName;
}
std::string AnimalBase::getName() const {
    return name;
}
void AnimalBase::eat(){
    std::cout << "Eating..." << std::endl;
}
AnimalBase::~AnimalBase() {
    // 可以为空,但需要定义
}

注意事项
1.有虚方法virtual void eat() = 0;virtual void eat();
前者表示纯虚方法,派生类必须使用override重写
后者表示一般虚方法,派生类可直接继承使用

  1. 定义一个类时,必要有构造器,可选析构器。如果声明了析构器,一定要写析构器的实现,否则使用语句
    delete 对象时会报错。

根据基类定义派生类
【Cat.h】

#ifndef CAT_H
#define CAT_H

#include "AnimalBase.h"
class Cat : public AnimalBase {
public:
    Cat(const std::string& inputName) : AnimalBase(inputName) {}  //派生类使用基类构造函数
    void eat() override;  //重写基类虚函数

};

class NormalCat : public AnimalBase {
public:
    NormalCat(const std::string& inputName) : AnimalBase(inputName) {}  //派生类使用基类构造函数
    void eat(){
         AnimalBase::eat();  //调用基类虚函数
         std::cout << "Normal Cat eats meat." << std::endl;
    }
};

#endif // CAT_H

【Cat.cpp】

#include "Cat.h"

void Cat::eat()
{
    std::cout << "Cat is eating" << std::endl;
}

实例化
有两种实例化方法
1.Animal *pig = new Pig("pp");

  1. Pig *pig = new Pig("pp");

①:
优势:可以调用不同派生类的方法(这些方法源自基类,可以被派生类重写),如

// 基类指针指向派生类对象
    Animal* animals[2];
    
    animals[0] = new Dog("Buddy");
    animals[1] = new Cat("Whiskers");
    
    // 通过基类指针调用派生类的重写函数
    for (int i = 0; i < 2; ++i) {
        animals[i]->speak();
    }

【输出】:

Buddy says: Woof!
Whiskers says: Meow!

劣势:不能调用派生类的特有成员方法,如果你需要调用Pig类特有的成员(在Animal中没有定义的成员),需要先进行类型转换(例如,使用 dynamic_cast<Pig*>(pig)

②:
优势:你可以直接调用Pig类的所有成员函数和属性,包括从 Animal继承的成员和 Pig类自己定义的成员
劣势:因为指针的静态类型是 Pig,它不能直接用于操作其他类型的派生类对象。如果你有多个不同的派生类对象(如 Dog, Cat等),并且想要统一处理它们,使用这种方式会不太方便。

25 如何创建并使用枚举类型

在 C++ 中,枚举(enum)是一种用户定义的数据类型,它由一组命名的整型常量组成。枚举类型可以提高代码的可读性和可维护性。以下是创建和使用枚举类型的步骤:

创建枚举类型

你可以使用 enum 关键字来定义一个枚举类型。例如:

enum Color {
    RED,
    GREEN,
    BLUE
};

在这个例子中,Color 是一个枚举类型,包含三个枚举常量:REDGREENBLUE。默认情况下,RED 的值为 0,GREEN 的值为 1,BLUE 的值为 2。

使用枚举类型

你可以像使用其他数据类型一样使用枚举类型。例如:

Color myColor = RED;

if (myColor == RED) {
    std::cout << "The color is red." << std::endl;
}

指定枚举常量的值

你也可以显式地指定枚举常量的值:

enum Color {
    RED = 1,
    GREEN = 2,
    BLUE = 3
};

枚举类(C++11 及以上)

在 C++11 及以上版本中,你可以使用 enum class 来定义强类型枚举,这样可以避免命名冲突:

enum class Color {
    RED,
    GREEN,
    BLUE
};

Color myColor = Color::RED;

if (myColor == Color::RED) {
    std::cout << "The color is red." << std::endl;
}

使用 enum class 定义的枚举类型需要使用作用域解析运算符(::)来访问枚举常量。

示例代码

以下是一个完整的示例,展示了如何创建和使用枚举类型:

#include <iostream>

enum class Color {
    RED,
    GREEN,
    BLUE
};

int main() {
    Color myColor = Color::RED;

    if (myColor == Color::RED) {
        std::cout << "The color is red." << std::endl;
    } else if (myColor == Color::GREEN) {
        std::cout << "The color is green." << std::endl;
    } else if (myColor == Color::BLUE) {
        std::cout << "The color is blue." << std::endl;
    }

    return 0;
}

这个示例程序将输出 "The color is red."。

26 构造器关键字 explicit

用于构造函数,防止隐式类型转换,当构造函数被标记为 explicit时,它只能通过直接初始化来调用,而不能通过隐式转换来调用。这有助于避免意外的类型转换,从而提高代码的安全性和可读性。
举例:

class Example {
public:
    explicit Example(int value) {
        // 构造函数逻辑
    }
};

Example ex1 = 10;  // 错误:隐式转换被禁止
Example ex2(10);  // 正确:直接初始化

27 两种构造器 a(p) 和a{p}

  • a(p) 在单参数时 可能被解释为函数声明,而不是实例初始化,出现报错:最令人困惑的解析(Most Vexing Parse)
  • a{p} 花括号初始化或列表初始化 避免隐式转换,执行更严格的类型检查
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容

  • 声明 本系列来至中国大学慕课-阚道宏老师的C++系列课程的相关课堂笔记,阚道宏老师讲解细腻、精准,由浅入深,徐徐渐...
    小马哥_MAYUE阅读 645评论 0 1
  • C语言经典例程100例 这篇文章主要介绍了C语言经典例程100例,经典c程序100例,学习c语言的朋友可以参考一下...
    縸_3354阅读 342评论 0 0
  • C语言的学习要从基础开始,这里是100个经典的算法-1C语言的学习要从基础开始,这里是100个经典的 算法 题目:...
    Poison_19ce阅读 1,129评论 0 0
  • 1. 类的默认成员函数 包括6个:构造函数、析构函数、拷贝构造函数、赋值运算符函数、取址运算符函数、const取址...
    zillo阅读 670评论 0 0
  • •1 C语言程序的结构认识 用一个简单的c程序例子,介绍c语言的基本构成、格式、以及良好的书写风格,使读者对c语...
    CONLYOUC阅读 8,701评论 9 66