C 语言应用开发中的注意事项
C 语言应用开发中的注意事项
C 语言作为一门经典的编程语言,广泛应用于系统编程、嵌入式开发和高性能计算等领域。然而,在进行 C 语言应用开发时,开发者需要关注一系列问题,以确保代码的正确性、性能和可维护性。
笔者在基于 C 语言开发 EdgeX Foundry 设备服务实践之中,遇到了一些问题,如全局变量的使用、.c 模块文件的划分、多线程编程注意事项、深浅拷贝问题以及编译链接时的多重定义问题。本文将对其展开讨论,并提供具体的代码示例和实用建议。
1.全局变量
全局变量是在函数外部定义的变量,其作用域从定义处开始到文件结束。它们在程序整个生命周期内存在,通常不会被轻易时放掉。可被程序中的任何函数访问。它能够方便多个函数共享数据,并减少函数参数传递的开销,但同时也不利于模块化编程和代码重用,增加调试难度。
以下展示的 global_pp
全局变量,在 EdgeX Foundry 设备服务中用于保存设备协议的详细信息,为部分关键代码:
#include "device_openvino.h"
#include "circular_buffer.h"
/* protocols */
typedef struct
{
char *input_model;
char *input_image_path; /* Uri */
char *device_name; /* CPU, GPU[0,1,...], NPU */
char *Score;
} protocol_properties;
protocol_properties *global_pp;
// malloc a protocol_properties
void func() {
if (!global_pp)
{
iot_log_debug(driver->lc, "malloc a global_pp (protocol_properties)");
global_pp = (protocol_properties *)malloc(sizeof(protocol_properties));
global_pp->input_model = NULL;
global_pp->input_image_path = NULL;
global_pp->device_name = NULL;
global_pp->Score = NULL;
}
}
int main() {
func();
return 0;
}
注意事项:
-
尽量减少全局变量的使用,优先考虑局部变量或函数参数。
-
使用有意义的命名(如 g_config),避免冲突。
-
在多线程环境中,使用锁机制确保线程安全。
2.源代码文件夹中 .c 模块文件的划分
模块化编程将程序分解为独立的模块,每个模块负责特定功能。这种方法能提高代码的可读性、可维护性和可重用性,同时便于团队协作开发。划分 .c 文件的指导思想有以下几点:
-
每个 .c 文件应包含一组相关功能的实现,例如字符串处理或网络通信。
-
避免单个 .c 文件过大,建议控制在 1000 行以内。
-
使用头文件 (.h) 声明对外接口,.c 文件实现具体逻辑。
-
头文件用于声明函数、宏、类型和全局变量,供其他模块使用。
以下是 EdgeX Foundry 设备服务中环形 buffer 缓冲区的模块设计:
//circular_buffer.h
#pragma once
#include <stddef.h> // 包含 size_t 的定义
// Circular buffer structure
struct circular_buffer {
struct infer_result* buffer; // storing data
size_t capacity; // Maximum buffer capacity
size_t size; // The current number of stored elements
size_t head; // Head index (write location)
size_t tail; // Tail index (read position)
};
struct circular_buffer* create_circular_buffer(size_t capacity);
...
//circular_buffer.c
#include "circular_buffer.h"
#include <stdlib.h>
#include "edgex_common.h"
// Initialize circular buffer
struct circular_buffer* create_circular_buffer(size_t capacity) {
//malloc a circular_buffer
struct circular_buffer* cb = (struct circular_buffer*)malloc(sizeof(struct circular_buffer));
if (cb == NULL) {
return NULL;
}
cb->buffer = (struct infer_result*)malloc(sizeof(struct infer_result) * capacity);
if (cb->buffer == NULL) {
free(cb);
return NULL;
}
//Init
cb->capacity = capacity;
cb->size = 0;
cb->head = 0;
cb->tail = 0;
return cb;
}
要点:
-
使用
#ifndef
和#define
或#pragma once
防止头文件重复包含。 -
头文件仅声明,不定义变量或函数。
-
保持头文件简洁,避免引入不必要的依赖。
3.多线程编程注意的问题
线程安全和互斥锁
在多线程程序中,多个线程可能同时访问共享资源,导致数据不一致。互斥锁(mutex)可用于保护临界区,确保同一时间只有一个线程操作共享数据。在 EdgeX Foundry 设备服务 SDK 设计中,服务实例结构体包含有互斥锁指针,在设备服务初始化时一并初始化 mutex
以便于在整个设备服务声明周期中进行资源同步管理:
/**
* @brief Structure representing an OpenVINO driver instance for managing inference operations.
*/
typedef struct openvino_driver
{
iot_logger_t *lc;
pthread_mutex_t mutex; // for synchronization
pthread_t inference_thread; // 推理线程句柄
bool stop_inference; // 控制推理线程停止的标志
} openvino_driver;
viod *openvino_inference_thread (){
...
// Thread-safe storage of results
pthread_mutex_lock(&driver->mutex);
push_result(global_cb, *results);
pthread_mutex_unlock(&driver->mutex);
}
线程间通信
线程间可通过共享内存、信号量或消息队列通信。例如,使用条件变量实现生产者-消费者模型。在 EdgeX Foundry 设备服务的后续开发中也会频繁出现。
4.深浅拷贝问题
浅拷贝: 只复制基本数据类型和指针,指针指向的内存不复制。
深拷贝: 复制所有数据,包括指针指向的内存。
在 EdgeX Foundry 设备服务开发中,协议资源 global_pp;
在 openvino_create_addr
中由深拷贝赋值,以此避免global_pp;
在 SDK 回调函数中被错误释放资源:
/**
* @brief Creates an address structure for the OpenVINO device service.
* @details This callback function is used by the EdgeX Foundry framework to create and populate a protocol_properties
* structure with OpenVINO-specific configuration data extracted from the provided protocols.
* @param impl Pointer to the implementation-specific data (cast to openvino_driver).
* @param protocols Pointer to the protocol data containing OpenVINO configuration.
* @param exception Pointer to store any exception data (not used in this implementation).
* @return devsdk_address_t A pointer to the populated protocol_properties structure, cast to the EdgeX address type.
*/
static devsdk_address_t openvino_create_addr(void *impl, const devsdk_protocols *protocols, iot_data_t **exception)
{
...
const iot_data_t *props = devsdk_protocols_properties(protocols, "Openvino");
//从设备获取协议资源
if (props)
{
result = iot_data_string_map_get_string(props, "Uri");
pp->input_image_path = result;
result = iot_data_string_map_get_string(props, "model");
pp->input_model = result;
result = iot_data_string_map_get_string(props, "CPU_GPU");
pp->device_name = result;
result = iot_data_string_map_get_string(props, "Score");
pp->Score = result;
}
// Allocate and populate new global protocol properties
// 深拷贝
global_pp->input_model = malloc(strlen(pp->input_model) + 1);
strcpy(global_pp->input_model,pp->input_model);
global_pp->input_image_path = malloc(strlen(pp->input_image_path) + 1);
strcpy(global_pp->input_image_path,pp->input_image_path);
global_pp->device_name = malloc(strlen(pp->device_name) + 1);
strcpy(global_pp->device_name,pp->device_name);
global_pp->Score = malloc(strlen(pp->Score) + 1);
strcpy(global_pp->Score,pp->Score);
return (devsdk_address_t)pp;
}
深拷贝的原因:
- 对象包含动态分配的内存。
- 需要独立副本,避免共享内存导致的问题。
总结
在 C 语言应用开发中,开发者需要综合考虑全局变量的合理使用、模块化设计的规范性、多线程编程的线程安全性、深浅拷贝的适用场景以及编译链接的正确性。
通过谨慎使用全局变量、合理划分模块、确保线程安全、正确处理拷贝和避免多重定义问题,可以编写出健壮、高效且易于维护的 C 语言程序。