C/C++ 聊聊结构体、指针、类

前言

QT可以调用很多第三方的C、C++库,了解一些基础知识是有必要的。C++能兼容大部分C的特性,有些是基于设计的考虑不兼容。提"兼容"只是为了指出可察的"延续性",不深入"兼容"这个话题。本篇侧重QT开发者的角度去理解,指针和结构体的知识是为类进行铺垫,主要是方便QT开发者阅读,对于C开发者来说,也仅是基础知识。

结构体

在C语言中,结构体是方便开发者自定义数据类型,准确的说是"数据聚合器",即把开发者需要的类型按顺序集合在一个地址空间中。

在C++中,基于设计哲学(OOP),结构体就是一个特殊的类,可以理解为一个轻量级的类,特殊点是:类成员默认为私有类型,结构体默认为公开类型。

特性 结构体(struct) 类(class)
成员变量
成员函数
继承
多态
模板
访问控制
默认访问权限​ public​ private

C风格的结构体用法

结构体与专属的操作是分类的

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>

typedef struct Buffer{
    char* data;
    size_t size;
} stBuffer;

void buffer_init(stBuffer* buf, size_t size) {
    if (buf == NULL) return;
    
    buf->data = (char*)malloc(size);
    if (buf->data != NULL) {
        buf->size = size;
        memset(buf->data, 0, size);
    } else {
        buf->size = 0;
    }
}

void buffer_free(stBuffer* buf) {
    if (buf == NULL) return;
    
    if (buf->data != NULL) {
        free(buf->data);
        buf->data = NULL;
    }
    buf->size = 0;
}

void buffer_copy(stBuffer* dst, const stBuffer* src) {
    if (dst == NULL || src == NULL || src->data == NULL) return;
    
    if (dst->data != NULL) {
        free(dst->data);
    }
    
    dst->data = (char*)malloc(src->size);
    if (dst->data != NULL) {
        memcpy(dst->data, src->data, src->size);
        dst->size = src->size;
    } else {
        dst->size = 0;
    }
}

void print_buffer(const stBuffer* buf, const char* name) {
    printf("%s: data=%p, size=%zu\n", name, (void*)buf->data, buf->size);
    if (buf->data != NULL) {
        printf("Content: ");
        for (size_t i = 0; i < buf->size && i < 10; i++) {
            printf("%02x ", (unsigned char)buf->data[i]);
        }
        printf("\n");
    }
}

int main() {
    printf("Buffer Management System Test\n");
    printf("=============================\n\n");
    
    stBuffer buf1;
    printf("Test 1: Initialize buffer\n");
    buffer_init(&buf1, 16);
    print_buffer(&buf1, "buf1");
    printf("\n");
    
    stBuffer buf2;
    printf("Test 2: Copy buffer\n");
    buffer_copy(&buf2, &buf1);
    print_buffer(&buf2, "buf2");
    printf("\n");
    
    printf("Test 3: Write data to buffer\n");
    if (buf1.data != NULL) {
        strcpy(buf1.data, "Hello Buffer!");
        printf("buf1 content: %s\n", buf1.data);
    }
    printf("\n");
    
    printf("Test 4: Deep copy with data\n");
    stBuffer buf3;
    buffer_copy(&buf3, &buf1);
    print_buffer(&buf3, "buf3");
    printf("buf3 content: %s\n", buf3.data);
    printf("\n");
    
    printf("Test 5: Free buffers\n");
    buffer_free(&buf1);
    buffer_free(&buf2);
    buffer_free(&buf3);
    
    print_buffer(&buf1, "buf1 after free");
    print_buffer(&buf2, "buf2 after free");
    print_buffer(&buf3, "buf3 after free");
    
    return 0;
}

C++风格的结构体用法

复制代码
#include <iostream>
#include <vector>
#include <string>
#include <cstring>

struct Buffer {
    std::vector<char> data;
    // 默认构造函数
    Buffer() = default;

    // 带大小参数的构造函数
    Buffer(size_t size) : data(size) {}
    
    // 从C字符串构造
    Buffer(const char* str) {
        if (str) {
            size_t len = strlen(str);
            data.resize(len);
            memcpy(data.data(), str, len);
        }
    }
    
    // 从std::string构造
    Buffer(const std::string& str) {
        data.resize(str.size());
        memcpy(data.data(), str.c_str(), str.size());
    }
    
    // 复制数据函数
    void copyFrom(const Buffer& other) {
        data = other.data;
    }
    
    
    // 清空缓冲区
    void clear() {
        data.clear();
    }
    // 打印缓冲区信息
    void printInfo(const std::string& name) const {
        std::cout << name << " - 大小: " << data.size();
        if (!data.empty()) {
            std::cout << ", 内容: ";
            // 显示十六进制数据
            for (size_t i = 0; i < data.size() && i < 15; i++) {
                printf("%02x ", static_cast<unsigned char>(data[i]));
            }
            if (data.size() > 15) {
                std::cout << "...";
            }
        }
        std::cout << std::endl;
    }
};

int main() {
    std::cout << "1. 不同构造函数:" << std::endl;
    Buffer buf1(10);  // 指定大小
    Buffer buf2("Hello C++ Buffer!");  // 从C字符串构造
    Buffer buf3(std::string("From std::string"));  // 从std::string构造
    Buffer buf4;  // 默认构造
    
    std::cout << "2. 复制操作:" << std::endl;
    buf4.copyFrom(buf2);
    buf4.printInfo("buf4 复制后");
    std::cout << std::endl;
    
    std::cout << "3. 清空缓冲区:" << std::endl;
    buf2.clear();
    return 0;
}

这里先不讲解C++的结构体里面为什么能加入函数,在类中讲解。

指针

"慎用"、"越界"、"安全释放",初学C时没少被吓唬。为了"高效",数据存储/读取、运算、排序中都有指针的影子,在C++中就很少,因为C++做了很多封装,比如常用的STL容器(迭代器就是一个节点指针),new一个类型/结构体/类对象(底层封装了malloc)。

在使用第三方库时,通常都会接触到指针函数和函数指针,这两个容易搞混,下面详细讲解。

指针函数

字面意思,返回指针的函数。QT调用第三方库时,有不少的机会遇到。以下代码我是之前分享的的博文(https://blog.csdn.net/liangyuna8787/article/details/144730835?spm=1001.2014.3001.5502【QT常用技术讲解】excel表格处理两种方式:QAxObject和qtxlsx)的代码片段

复制代码
#include "xlsxdocument.h"
#include "xlsxworksheet.h"
void MainWindow::saveexcel(QTableWidget *tableWidget, const QString &fileName)
{
    QXlsx::Document xlsx;
    QXlsx::Worksheet *worksheet = xlsx.currentWorksheet();
 
    // 设置表头和数据
    for (int row = 0; row < tableWidget->rowCount(); ++row) {
        for (int col = 0; col < tableWidget->columnCount(); ++col) {
            QTableWidgetItem *item = tableWidget->item(row, col);
            if (item) {
                worksheet->write(row + 1, col + 1, item->text());
            } else {
                worksheet->write(row + 1, col + 1, "");
            }
        }
    }
    //合并表头
    xlsx.mergeCells(QXlsx::CellRange(1, 1, 1, tableWidget->columnCount()));
    // 保存到文件
    xlsx.saveAs(fileName);
}

可以看到有两个函数返回了指针,一般涉及层级、节点的库,都会用指针进行高效操作。

函数指针

它是一个指向某种类型函数的指针变量,即这个指针变量指向某个类型(编译器会检测类型是否匹配)的函数。

通俗的讲,它首先是一个变量,因为具备指针的"借尸还魂"的指向特性,可以指向某个代码片段。

比较常见的案例是C、C++动态库的函数调用,以下分享Linux下C、C++动态库调用的案例

C函数指针--动态库调用

复制代码
// 动态库文件mathlib.c
int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

//编译命令:gcc -fPIC -shared -o libmath.so mathlib.c

用dlopen方式调用动态库,以下提供两种函数指针类型定义的方法

复制代码
// main.c
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>  // 必须包含此头文件

// 函数指针类型定义(与动态库中的函数签名一致)
int (*add)(int, int);
typedef int (*multiply_func_t)(int, int);

int main() {
    void *handle;
    multiply_func_t multiply;
    char *error;

    // 打开动态库
    handle = dlopen("./libmath.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "dlopen error: %s\n", dlerror());
        exit(EXIT_FAILURE);
    }

    // 清除之前的错误
    dlerror();

    // 获取 add 函数地址
    *(void **)(&add) = dlsym(handle, "add");
    error = dlerror();
    if (error != NULL) {
        fprintf(stderr, "dlsym 'add' error: %s\n", error);
        dlclose(handle);
        exit(EXIT_FAILURE);
    }

    // 获取 multiply 函数地址
    *(void **)(&multiply) = dlsym(handle, "multiply");
    error = dlerror();
    if (error != NULL) {
        fprintf(stderr, "dlsym 'multiply' error: %s\n", error);
        dlclose(handle);
        exit(EXIT_FAILURE);
    }

    // 调用函数
    printf("add(3, 5) = %d\n", add(3, 5));
    printf("multiply(4, 6) = %d\n", multiply(4, 6));

    // 关闭动态库
    dlclose(handle);
    return 0;
}

C++函数指针--动态库调用

动态库不仅仅可以传递函数,类也可以传递,以及直接传递类中的函数。编写动态库时,需要额外加上extern "C" {},之前的博文分享传递类和函数的样例:

复制代码
#pragma once
#include <iostream>
 
class  MyClass {
public:
    MyClass();
    ~MyClass();
    void printMessage(const char* msg);
};
 
//普通函数需要使用extern "C"声明,外部才可调用
#ifdef __cplusplus
extern "C" {
#endif
MyClass* createMyClass();      // 创建对象
void destroyMyClass(MyClass*); // 销毁对象
 
#ifdef __cplusplus
}
#endif

详细讲解请查看博文:【Linux C/C++开发】编译及引用so动态库

注意了,以上分享函数指针的样例,是让你感知到指针强大的"跨域"引用能力,能更好的理解

从C结构体的延伸

类是面向对象编程(oop)的核心设计,C++本身也是为了改进C语言,从底层语言向高级语言迈进(调侃~)。

前面已经分享了在C++结构体是一个特殊的类。从C语言的角度来看,也可以理解为C++中的类是从结构体延伸出去的成熟体。

这个是"C语言的角度":了解C语言基础知识的人都知道C是没有类的,但结构体+函数指针是可以看出类的雏形,下面提供个代码案例:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 模拟一个"矩形类"
typedef struct Rectangle {
    // 数据成员
    double width;
    double height;
    
    // 函数指针(模拟成员函数)
    double (*area)(struct Rectangle* self);
    double (*perimeter)(struct Rectangle* self);
    void (*setSize)(struct Rectangle* self, double w, double h);
    void (*display)(struct Rectangle* self);
} Rectangle;

// 成员函数实现
double rectangle_area(Rectangle* self) {
    return self->width * self->height;
}

double rectangle_perimeter(Rectangle* self) {
    return 2 * (self->width + self->height);
}

void rectangle_setSize(Rectangle* self, double w, double h) {
    if (w > 0) self->width = w;
    if (h > 0) self->height = h;
}

void rectangle_display(Rectangle* self) {
    printf("矩形: 宽=%.2f, 高=%.2f, 面积=%.2f, 周长=%.2f\n",
           self->width, self->height, self->area(self), self->perimeter(self));
}

// 构造函数
Rectangle* Rectangle_create(double width, double height) {
    Rectangle* rect = (Rectangle*)malloc(sizeof(Rectangle));
    if (rect) {
        rect->width = width;
        rect->height = height;
        
        // 绑定函数指针
        rect->area = rectangle_area;
        rect->perimeter = rectangle_perimeter;
        rect->setSize = rectangle_setSize;
        rect->display = rectangle_display;
    }
    return rect;
}

// 析构函数
void Rectangle_destroy(Rectangle* rect) {
    if (rect) {
        free(rect);
    }
}

//---↑↑↑↑↑--以上是结构体"矩形类"相关定义-----↑↑↑↑↑
//---↓↓↓↓↓--以下模拟"继承"特性--------------↓↓↓↓↓
// 模拟继承:彩色矩形继承自矩形
typedef struct ColoredRectangle {
    Rectangle base;  // 基类(模拟继承)
    char color[20];
    
    // 新增的函数指针
    void (*setColor)(struct ColoredRectangle* self, const char* color);
    void (*displayColor)(struct ColoredRectangle* self);
} ColoredRectangle;

// 子类特有的函数
void coloredRectangle_setColor(ColoredRectangle* self, const char* color) {
    strncpy(self->color, color, sizeof(self->color) - 1);
    self->color[sizeof(self->color) - 1] = '\0';
}

void coloredRectangle_displayColor(ColoredRectangle* self) {
    printf("彩色");
    self->base.display((Rectangle*)self);  // 调用基类方法
    printf("颜色: %s\n", self->color);
}

// 子类构造函数
ColoredRectangle* ColoredRectangle_create(double width, double height, const char* color) {
    ColoredRectangle* crect = (ColoredRectangle*)malloc(sizeof(ColoredRectangle));
    if (crect) {
        // 初始化基类部分
        crect->base.width = width;
        crect->base.height = height;
        
        // 绑定基类函数指针
        crect->base.area = rectangle_area;
        crect->base.perimeter = rectangle_perimeter;
        crect->base.setSize = rectangle_setSize;
        crect->base.display = rectangle_display;
        
        // 初始化子类部分
        strncpy(crect->color, color, sizeof(crect->color) - 1);
        crect->color[sizeof(crect->color) - 1] = '\0';
        
        // 绑定子类函数指针
        crect->setColor = coloredRectangle_setColor;
        crect->displayColor = coloredRectangle_displayColor;
    }
    return crect;
}

// 子类析构函数
void ColoredRectangle_destroy(ColoredRectangle* crect) {
    if (crect) {
        free(crect);
    }
}
//---↑↑↑↑↑--以上模拟"继承"特性--------------↑↑↑↑↑

int main(){
    // 1. 创建和使用矩形对象
    printf("1. 矩形类使用:\n");
    Rectangle* rect = Rectangle_create(10.0, 5.0);
    rect->display(rect);
    
    rect->setSize(rect, 8.0, 6.0);
    printf("修改大小后: ");
    rect->display(rect);
    printf("面积: %.2f\n", rect->area(rect));
    printf("\n");
    
    // 2. 继承使用
    printf("2. 继承使用:\n");
    ColoredRectangle* credRect = ColoredRectangle_create(12.0, 8.0, "红色");
    credRect->displayColor(credRect);
    
    credRect->setColor(credRect, "蓝色");
    credRect->base.setSize((Rectangle*)credRect, 15.0, 10.0);
    printf("修改后: ");
    credRect->displayColor(credRect);
    printf("\n");

    // 3. 资源清理
    printf("4. 资源清理:\n");
    Rectangle_destroy(rect);
    ColoredRectangle_destroy(credRect);

    return 0;
}

以上代码来源于AI,逻辑是OK的,本人没有编译,感兴趣的自己编译出问题,可以让AI处理。

C语言是面向过程的编程语言,C++是面向对象的编程语言。不只是设计理念,语法、编译器也不一样。以下是几个演变过程:

  1. 从C的结构体到C++的类:C语言的结构体只能包含数据成员,而C++的结构体可以包含成员函数、访问控制(public、private、protected)、继承、虚函数等。

  2. 虚函数表(vtable):为了实现多态,C++引入了虚函数。每个包含虚函数的类(或结构体)都会有一个虚函数表,这是一个函数指针数组,每个虚函数对应一个指针。每个对象会有一个隐藏的指针(vptr)指向这个表。当调用虚函数时,通过vptr找到虚函数表,再通过偏移量调用正确的函数。

  3. 成员函数 :非虚成员函数在编译时就可以确定地址,因此不需要通过指针间接调用。它们与普通函数类似,只是隐含了一个指向对象的指针(this指针),自动在函数参数列表开头添加一个隐藏参数:ClassType* this

QT的信号-槽机制其实也是在维护一个"表",某个地方发出信号之后,信号就遍历这个"信号-槽"表,匹配上信号的就触发槽函数的调用。

虚函数表,以及指向对象的指针(this指针)机制,都是C++为了让类准确的找到虚函数/成员函数。

以上只是从映射表的逻辑来描述C结构体和C++的关联性,辅助理解,并非是全部执行逻辑。

类的特性

以上是与C有关的知识,下面记录下QT开发中使用到的类特性。

类的封装

类默认是private访问控制,即私有性质,外部无法直接调用。除了构造函数,需要通过显示接口对外暴露调用,尽可能避免外部代码直接修改内部状态。

比如myclass类有一个成员变量bool m_status;,如果设置为public访问,就会被外部代码直接引用并修改,可以这样操作,但是不安全,违背封装性,通常是提供一个对外暴露的接口函数bool getmyclassstatus() const;来获取支持,如果需要修改此值,也是提供对外暴露的接口函数 void setstatus(bool);来使用。

private访问控制不代表不能访问,友元(friend)机制就是一个特例机制。

类的继承

定义一个新类(派生类 / 子类)基于已有类(基类 / 父类),从而复用代码、扩展功能、实现多态**。**

复制代码
//常用的单继承
class MainWindow : public QMainWindow{
};
//可以多继承
class Document : public Printable, public Serializable { 
};
基类中的访问权限 继承方式 在派生类中的访问权限
public public public
protected public protected
private public 不可访问
public protected protected
protected protected protected
private protected 不可访问
public private private
protected private private
private private 不可访问

类的多态

使用统一的接口处理不同的数据类型。

运行时多态(动态多态):虚函数机制

QT开发中,经常接触的有重新虚函数,比如事件过滤器eventFilter()是QObject 类中已经声明了虚函数,

复制代码
virtual bool eventFilter(QObject *watched, QEvent *event);

默认实现是 返回 false (表示不处理事件,继续传递),如果需要拦截某个对象的事件,需要重写(override)eventFilter()

复制代码
protected:
    bool eventFilter(QObject *watched, QEvent *event) override;//重写事件过滤器

在基类中virtual 标明的虚函数,目的规范接口的统一入口,引用的类需要自己具体实现业务逻辑。

继承+多态可以有以下一个现象:

A类继承了base类,重写了event函数,B类继承了A类,B类也可以重写event函数。

有一种多重套娃的感觉,但是如果在A类中重写event函数时,加上final,就可以防止B类进一步重写。

复制代码
class A {
    // final防止进一步重写
    void event() override final {
        cout << "Derived(final)" << endl;
    }
}
编译时多态(静态多态):函数重载、运算符重载、模板
函数重载

同一函数名,参数类型不同

复制代码
class Calculator {
public:
    // 同一函数名,不同参数列表
    int add(int a, int b) {
        cout << "整数加法: ";
        return a + b;
    }
    
    double add(double a, double b) {
        cout << "浮点数加法: ";
        return a + b;
    }
    
    string add(const string& a, const string& b) {
        cout << "字符串连接: ";
        return a + b;
    }
    
    int add(int a, int b, int c) {
        cout << "三个整数加法: ";
        return a + b + c;
    }
};

// 使用
Calculator calc;
cout << calc.add(5, 3) << endl;           // 整数加法: 8
cout << calc.add(2.5, 3.7) << endl;        // 浮点数加法: 6.2
cout << calc.add("Hello", "World") << endl; // 字符串连接: HelloWorld
cout << calc.add(1, 2, 3) << endl;         // 三个整数加法: 6
运算符重载

对常用的运算符"+"、"-"、"=="等进行重载

复制代码
class Complex {
private:
    double real, imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // 运算符重载
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }
    
    Complex operator-(const Complex& other) const {
        return Complex(real - other.real, imag - other.imag);
    }
    
    // 关系运算符重载
    bool operator==(const Complex& other) const {
        return real == other.real && imag == other.imag;
    }
    
    // 输出运算符重载(友元函数)
    friend ostream& operator<<(ostream& os, const Complex& c) {
        os << c.real << " + " << c.imag << "i";
        return os;
    }
};

// 使用
Complex c1(3, 4), c2(1, 2);
Complex c3 = c1 + c2;  // 多态:+运算符根据操作数类型执行不同操作
cout << c3 << endl;    // 输出: 4 + 6i
模板

需要进行多种类型的运算时,抽象出一个模板比函数重载更好管理。

复制代码
// 没有模板 - 需要为每种类型重载函数
int max(int a, int b) { return (a > b) ? a : b; }
double max(double a, double b) { return (a > b) ? a : b; }
string max(const string& a, const string& b) { return (a > b) ? a : b; }

// 有模板 - 一个模板处理所有类型
template<typename T>
T max(T a, T b) { return (a > b) ? a : b; }

类中的元素也可以做成模板

复制代码
template<typename T>
class Stack {
private:
    vector<T> elements;
    
public:
    void push(const T& value) {
        elements.push_back(value);
    }
    
    T pop() {
        if (elements.empty()) {
            throw out_of_range("Stack is empty");
        }
        T value = elements.back();
        elements.pop_back();
        return value;
    }
    
    T top() const {
        if (elements.empty()) {
            throw out_of_range("Stack is empty");
        }
        return elements.back();
    }
    
    bool empty() const {
        return elements.empty();
    }
    
    size_t size() const {
        return elements.size();
    }
};

// 使用
int main() {
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);
    
    while (!intStack.empty()) {
        cout << intStack.pop() << " ";  // 输出: 3 2 1
    }
    cout << endl;
    
    Stack<string> stringStack;
    stringStack.push("World");
    stringStack.push("Hello");
    cout << stringStack.pop() << " " << stringStack.pop() << endl;  // Hello World
    
    return 0;
}
相关推荐
QQ_4376643141 小时前
分布式RPC网络框架
网络·c++·分布式·rpc
fy zs1 小时前
Linux线程互斥与同步
linux·c++
老王熬夜敲代码1 小时前
万能引用、完美转发
c++·笔记
FMRbpm1 小时前
栈练习--------(LeetCode 739-每日温度)
数据结构·c++·算法·leetcode·新手入门
山峰哥1 小时前
从指针到智能体:我与C++的二十年技术进化与AI革命
大数据·开发语言·数据结构·c++·人工智能
mjhcsp1 小时前
P1906 凯撒密码洛谷(mjhcsp)
c++
大袁同学1 小时前
【C++完结篇】:深入“次要”但关键的知识腹地
开发语言·数据结构·c++·算法
明洞日记1 小时前
【数据结构手册006】映射关系 - map与unordered_map的深度解析
数据结构·c++
凌康ACG1 小时前
Sciter设置图标、设置进程名称
c++·sciter