前言
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++是面向对象的编程语言。不只是设计理念,语法、编译器也不一样。以下是几个演变过程:
-
从C的结构体到C++的类:C语言的结构体只能包含数据成员,而C++的结构体可以包含成员函数、访问控制(public、private、protected)、继承、虚函数等。
-
虚函数表(vtable):为了实现多态,C++引入了虚函数。每个包含虚函数的类(或结构体)都会有一个虚函数表,这是一个函数指针数组,每个虚函数对应一个指针。每个对象会有一个隐藏的指针(vptr)指向这个表。当调用虚函数时,通过vptr找到虚函数表,再通过偏移量调用正确的函数。
-
成员函数 :非虚成员函数在编译时就可以确定地址,因此不需要通过指针间接调用。它们与普通函数类似,只是隐含了一个指向对象的指针(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;
}