Tag Archives: C

【C语言深入】陷阱:数组指针作为函数参数返回

再来看一个指针问题,同样的来自一个本科生的代码。这段代码想要实现将一个全是小写字母的字符串转换成对应的大写字母字符串:

char *covert_to_upper_case(char *string) {
    char p[100];
    int i = 0;
    
    for(; i < strlen(string); i++ ) {
            p[i] = string[i] - ('a' - 'A'); 
    }
    p[i] = '\0';
    
    return p;
    
}

然而这段代码没有能实现期望的功能。原因如下:

  1. 主程序调用convert_to_upper_case()函数后,堆栈为p分配了内存空间;
  2. 函数体正确修改了p对应字符数组的内容,并将p的首地址作为指针返回;
  3. 函数返回后,所有临时变量从堆栈中弹出,包括p[100];
  4. 主程序得到返回的指针,对其进行解析。然而指针指向的字符数组此时已经从堆栈中弹出,解析后的数据无法被定义。

要想正确实现对应的功能,应该将目标指针作为额外参数传递给该函数,并由上层调用者提供内存空间的创建。当然也可以使用malloc()将内存分配在堆中,但是需要注意使用对应的free()释放空间,否则会有内存泄露的问题。

【C语言深入】指针的一个错误赋值

关于指针总是有说不完的故事。

最近给本科的学生带Embedded System课程设计,遇到了一个非常奇怪的bug。有一段代码需要实现I2C通信,核心代码已经由软件库提供了,学生只需要设置结构体后调用API即可。一个学生的代码是这样的:

struct I2C_CONFIG {
  // ...
  char *i2c_buff;
  int length;
  // ...
};

struct I2C_CONFIG cfg;
char *i2c_buff;

void I2C_init() 
{
  // ...
  cfg.buff = i2c_buff;
  cfg.length = sizeof(buff);
  // ...
}

void I2C_send(new_buff)
{
  // ...
  i2c_buff = new_buff;
  I2C_MasterTransferData(LPC_I2C1, cfg);
  // ...
}

初看一下没有什么问题:在I2C_init()函数中首先对结构体cfg进行初始化,而在I2C_send()函数中设置了需要发送的数据指针,之后使用I2C的API发送数据。

因为代码一直无法实现期望的功能,我又仔细看了一下其中的蹊跷。我注意到,这段代码中使用了一个中间变量:char *i2c_buff。在I2C_init()中虽然将cfg.buff指向了i2c_buff,但是因为cfg.buff本身也是指针变量,而非"指向指针的指针",所以这里只实现了简单的按值传递,即将i2c_buff的值 (初始值为0) 赋给了cfg.buff。之后虽然在I2C_send()中修改了临时变量i2c_buff指向的位置,但却没有影响到cfg.buff中的内容,cfg.buff依然指向之前i2c_buff初始化时指向的内存地址,所以需要发送的缓冲指针new_buff其实并没有传递给之后的I2C_MasterTransferData()函数!为了解决这个问题,必须将更改后的i2c_buff的值再次赋给cfg.buff,即:

void I2C_send(new_buff)
{
  // ...
  i2c_buff = new_buff;
  cfg.buff = i2c_buff;
  I2C_MasterTransferData(LPC_I2C1, cfg);
  // ...
}

另外这段代码还有一个不容易注意的bug,就是在I2C_init()中使用了sizeof()来判断buffer的大小。因为sizeof()函数得到的只是数据类型的大小,所以对于指针char *i2c_buff来说,sizeof(i2c_buff) = 4,而不会返回buffer的实际大小。指针的大小并不等于指针指向缓冲的大小!

【C语言深入】C/C++变量命名规范

目前主流的C/C++命名风格有两种:一种是Windows风格的匈牙利命名法,主要是采用类型前缀 + 变量名首字母大写,另一个就是Unix/Linux命名习惯。我自己采用的是基于Unix的变种,融合了匈牙利命名法的一些优点,在这里分享给大家。

变量名的组成:(模块名) +  (作用域) + (类型前缀) + 变量名 + (变量名后缀),解释如下:

  • 变量名 以小写的英文字母构成,词与词之间用下划线连接,如key_value, data_src; 不可使用数字,不混用大小写;
  • 模块名 声明该变量属于的模块,防止模块与模块的命名冲突。如timer_prescalar_value, DMA_channel_name等;
  • 作用域前缀 (Scope Prefix) 标注变量的作用域,提高代码可读性:
    g_: 全局变量;
    n_: 局部变量;
    t_: 中间变量;
    s_: static静态变量;
  • 类型前缀 (Type Prefix) 指明变量的数据类型:
    ptr_: 指针变量,在程序中临时需要使用指针时,也常简写为p_,如*p_src;
    h_: 句柄,如h_file;
    n_: 整形,s_: 短整形,l_: 长整形, u_: 无符号整型,可增加数据位数,如u32;
    ch_: 字符型变量;
    f_: 浮点,d_: 双精度浮点;
    b_: boolean;
    by_: byte字节型(关注数据的位特性,需要位操作的情况下使用);
    reg_: 表示寄存器;
  • 后缀 (Suffix) 指明变量的性质:
    _src: 源,_dst: 目的;
    _str: 字符串;
    _t: 在声明数据类型时使用,表示为自定义的数据类型,如u32_t;
    _st: 表示为结构体;
    _buff: 数据缓冲, msg_buff;
    _arr, _a, _m: 数组或矩阵;

变量名的取名规则:

  • 循环控制变量 i, j, k, m, n,除循环控制外应避免使用这些变量名称;
  • 函数名 使用(模块名 + )动词 + 名词的形式,同样小写 + 下划线:sys_find_file(), IO_get_data(). 后者因为IO为专用名词故破例使用大写;
  • 类名或结构体名 使用首字母大写加下划线连接:如Mystring, Datetime_type;
  • 私有类成员 Private使用下划线_前缀,如_data_src_ptr, _init_module();
  • 宏定义或常量 使用全部大写:如MAX_NUMBER, LOOP_NUMBER;
  • 缩写 使用能广泛接受的缩写:如add, ans, avg, chk, cnt, col, ctrl, def, del, dst, disp, err, freq, idx, init, len, min, max, mid, msg, num, opt, pos, ptr, recv, res, ret, src, str, sub, num, ts (timestamp), val等。

本网站的所有实例代码和项目程序都将按此命名规范进行编写。

【C语言深入】C语言的函数指针

函数指针是C语言指针中的一个分支:函数指针是指向函数地址的指针。和一般的指针一样,函数指针可以大大增强编程时的灵活性。这篇博文根据我的理解,简单介绍了自己对于函数指针的理解。

一、函数名的本质

在介绍函数指针之前,我们先来理解一下究竟什么是函数。以下一段代码定义了名为fun1(),接受int并返回int参数的函数:

int fun1(int x) {	// fun1是函数名
	return x + 1;	// 函数体
}

从一般的理解角度来看,fun1是函数的函数名。之所以要定义函数名,是因为在程序的其他位置要调用该函数时,可以直接使用这个别名。这种使用别名的方法和变量相似,但又不完全一样。对一个变量x而言,其意义如下:

物理地址 数值  说明
0x20000000        30 <-- x是该数值单元的别名

x是内存单元0x20000000所对应的内存单元。此处x = 30,而x的地址&x = 0x20000000.

那么对一个函数而言,函数名又代表什么呢?假设之前的fun1函数在编译后被放置在0x08000200地址,则fun1对应的内存结构如下所示:

物理地址 数值 说明
fun1 (0x08000200) fun1_entry <-- fun1是函数的入口地址

在这里,函数名代表一个记录了函数入口地址的存储单元的物理地址。可见,函数名的本质是地址。在编译阶段,函数名被转化成为对应的地址。在使用 xxxx() 函数调用的语法时,该地址被载入程序计数器PC,函数参数及当前现场被弹入堆栈。最后进行函数的实际跳转和执行。

二、函数指针是什么

对于一般变量而言,指针可以指向变量的地址,并修改变量的内容:

物理地址 数值  说明
0x20000000         30 <-- x是整形变量
0x20000004  0x20000000 <-- p是指向x的指针

这里p是指向x的指针,(即p = &x)。此时p的内存单元所存储的是x单元的物理地址,通过 *p 解析地址之后就可以访问或修改x单元的内容。同样的,函数也有其对应的指针 - 函数指针。函数指针是一种特殊的指针,其指向的对象不是变量而是函数。函数指针指向目标函数的入口地址(首地址)。这里我们定义一个指向fun1函数的函数指针pf:

int fun1(int);		// fun1是一个(含int输入参数和int返回参数的)函数
int (*pf)(int);		// pf是(指向返回int型,含int参数函数的)函数指针
pf = &fun1;

/* 也可以直接写成: */
int (*pf)(int) = &fun1;

/* 如果需要调用函数指针对应的函数,可以写为:*/
(*pf)();		// 等价于fun1()

到这里,我们回顾上一节中说到的函数名。其实函数名也可以理解成为const型的函数指针。所以在c语言中,以下调用也是合法的:

(*fun1)()		// 等价于fun1()
pf = fun1		// 等价于pf = &fun1;
/* 但是fun1的值不能被修改 */
fun1 = fun2		// 错误!fun1是const类型的指针

函数指针的声明较为冗长,如果需要定义多个同类型的函数指针。可以通过typedef定义一个函数指针类型,从而进行简化:

typedef int (*PF)(int); // 声明PF是一个函数指针类型
PF pf1 = fun1;
PF pf2 = fun2;

三、函数指针的应用场景

以上说明了如何定义函数指针,下面介绍函数指针的应用场景。函数指针最常见的应用还是作为回调函数的参数。一般在事件驱动的程序框架中,当对应事件发生时,需要触发对应的处理函数。以下代码实现了在初始化阶段,将事件与对应的处理函数关联(假设set_event_callback()是实现该功能的系统函数):

typedef int (*CALLBACK)(int);

#define EVENT_1		(0x01)
#define EVENT_2		(0x02)

int set_event_callback(const int e, CALLBACK);

void init_callbacks() {
	CALLBACK pf1, pf2;
	set_event_callback(EVENT_1, pf1);
	set_event_callback(EVENT_2, pf2);
}

在set_event_callback中,函数指针pf作为参数传递给函数使用。在基于事件编程的框架中(如一般的GUI库),函数指针经常以作为回调函数的方式出现。函数指针的另一个应用就是,根据当前程序进程的不同,要在不同条件下调用不同的处理函数:

void change_function(int nEvent, PF *ppf) {

	switch (nEvent) {
		case 0: 
			*ppf = fun1;
			break;
		case 1: 
			*ppf = fun2;
			break;
		default:
			*ppf = fun3;
	}
}

这里利用了函数指针指向的函数是可以改变的(而非函数名是const型的)。change_function() 将函数指针的地址(指向函数指针的指针)作为参数传入,并根据当前nEvent的情况更改原函数指针指向的函数。

函数指针的另一个应用场景,是允许程序在只知道函数物理地址(但不知道具体的函数名)的情况下进行函数跳转。相信大家都还记得这个经典的C语言面试题:

(*(void(*)(void))0)();
// 或
((void(*)(void))0)();

此语句的本质就是将0内存位置强制转换成为了函数指针,并调用了该函数指针指向的函数。以下代码解释了此处是如何将0地址转换成函数指针的:

typedef void (*PF)(void);

// (*(void(*)(void))0)() 等价于
(*(PF)0)()
// 或
((PF)0)()

函数指针的更高级用法就是组成函数指针数组,或和指向函数指针的指针配合使用。在此不再继续介绍,感兴趣的读者可自行研究。

参考资料

[1] Kenneth A.Reek, C和指针(第二版), 2008, 人民邮电出版社

[2] Brian W. Kernighan / Dennis M. Ritchie, The C Programming Language (Second Edition), 1989, Prentice Hall

[3] C语言中文网,C语言函数名与函数指针详解http://c.biancheng.net/cpp/html/496.html

【C语言深入】陷阱:一个Break在if语句中的误用

C语言以高效著称,但其也存在很多晦涩的语法。让我们来看下面这段(真实的)代码:

switch (cmd) {
	case 0: break;
	case 1:
		if (bReceived == 1) {
			if (bNotReady == 0) {
				break; // 此处的break有误
			}
			do_something();
		}
		// 本意是满足条件跳出if, 不执行do_something()
		do_otherthing();
		break;
	default:
		break;
}// 却意外地跳出了switch, 从而没有执行do_otherthing()

以上代码的break意外跳出了case语句。之所以会产生这样的错误,是因为break在for和while中有类似的应用。我的这位朋友是在没有意识的情况下写出了上面这段代码。不仅如此,编译器(甚至高级的排错工具)对此也不会产生疑问,因为break在case语句之中是可以合法存在的。这里我用VS 2010测试了一下,如果该语句不在case语句下,编译器会提示break用法错误。

该错误可以通过将if的逻辑反转来解决:

switch (cmd) {
	case 0: break;
	case 1:
		if (bReceived == 1) {
			// 如果bNotReady == 0,将不执行do_something()
			if (bNotReady != 0) {
				do_something();
			}
		}
		do_otherthing();
		break;
	default:
		break;
}

 

【修订历史】

2015/12/02: V1.1 增加了对原代码的分析说明,增加了修改后的代码。

【C语言深入】陷阱:一个优先级导致的BUG

虽然我在工作中已经非常注意优先级的问题,可还是不小心犯了一个优先级导致的错误:

while (USART1->SR & USART_SR_RXNE == 0);

这句话的本意是等待SR寄存器的某位变为1才继续往下执行,而实际上,因为"=="的优先级大于"&",这句话等同于:

while (USART1->SR & (USART_SR_RXNE == 0));
//而 (USART_SR_RXNE == 0) 一直不成立,从而这句话等价于:
while (USART1->SR & 0);

即 while (0); 所以每次执行到该句话时,不管当前SR寄存器的内容是什么,都会直接跳过while。为了防止出现优先级错误,正确的写法应该是:

while ((USART1->SR & USART_SR_RXNE) == 0);

与此类似的错误还有:

nValue = nByte << 3 + 8;

这句语句的本意是将nByte左移3位后的值加上8赋给nValue,然而实际上因为算术运算符"+"的优先级大于以为运算符"<<",于是这句话变为:

nValue = nByte << (3 + 8);

// 即
nValue = nByte << 11;

可见优先级关系在这里导致了完全错误的结果。

【C语言深入】C语言运算符优先级

运算符优先级是C语言重要的组成部分,也是程序bug的常见产生源。需要特别注意的有:

  1. 赋值在C语言中的优先级是最低的;
  2. 位运算的优先级高于逻辑运算;
  3. 关系运算符的优先级低于移位,但是高于位运算;
  4. 单目运算的优先级最高;
  5. 同优先级的运算符按照从左向右的顺序解释。

Table 1. C语言运算符优先级

成员括号 ()   []   ->   .
单目运算  !   ~   ++   --   -   (type)   *   &   sizeof
算数运算 *   /   %   +   -
移位运算 <<   >>
关系运算 >   <   >=   <=
==   !=
位与运算 &
^
|
逻辑运算  &&
||
三目运算  ?:
赋值运算  =   +=   -=

 

【版本修订】

v1.1 2015.12.05 增加了表格可读性;增加了注意事项列表。

C | 简易拼音输入法

Automatic.dai : 之前在Ourdev论坛上看到有人用C51设计了拼音输入法,代码还没有看,但是对于其实现原理很感兴趣,所以自己简单尝试了一下。

主要原理:

建立一张汉字索引表,主键为拼音,如”bao”,内容为汉字码表,如”包宝饱报抱保苞胞褒雹堡豹鲍暴爆剥薄瀑”。根据用户输入的拼音去查找对应的汉字码表,之后再根据用户输入的数字输出对应的汉字。这种方式很像LUA的table数据结构,用LUA写这个程序会简单很多,之后我会尝试用LUA写一个版本。

程序优点:

索引为拼音字符串的指针,检索阶段不涉及具体汉字,速度快、原理简单。

程序缺点:

因为只是一个演示程序,这个程序还存在不少缺点。比如使用顺序方式查找,效率低,并且越在表后的数据,检索的速度就越慢。要解决这个问题,可以使用二次查找的方法加快速度,即首先查找‘b’,之后直接散转查找’b’开头的拼音 ”ba”, ”ban”, ”bang” 等。这还需要建立一张映射表,读者可自行尝试。

源代码:

/*--------------------------------------------------------------------------
* 项目名称 :简易拼音输入法
* 程序名称 :主程序
* 项目组织 :云飞工作室
* 程序编写 :戴晓天
* 邮  箱 :automatic.dai@gmail.com
* 版  本 :V0.1.111124(beta)
----------------------------------------------------------------------------
* 程序说明 :
 本程序利用建立索引的方法实现了简单的拼音输入法。
  目前仅支持a、b开头的拼音码,可以自行扩展。
----------------------------------------------------------------------------
* 修订说明 :
  暂无修订。
--------------------------------------------------------------------------*/
#include "stdio.h"
#include "string.h"
#include "conio.h"

typedef struct _PY_Index
{
     char *_PyMa;       // 存放拼音的拼音码
     char *_PYTable;    // 对应的中文汉字表
}PY_Index;
PY_Index PY_Ma_Talbe[] = {
    {"a"    ,"阿啊嗄呵锕吖腌錒"},
    {"ai"   ,"哎爱唉碍哀埃挨矮蔼皑癌艾隘暧"},
    {"an"   ,"按安案暗俺岸胺氨庵"},
    {"ang"  ,"昂盎肮醃醠枊肮岇骯卬"},
    {"ao"   ,"奥澳傲懊熬袄凹敖翱"},

    {"b"    ,"不把吧爸必"},
    {"ba"   ,"把吧爸巴拔八"},
    {"bai"  ,"白百佰柏摆败拜稗"},
    {"ban"  ,"办半搬板伴"},
    {"bang" ,"棒帮榜镑蚌"},
    {"bao"  ,"包宝饱报抱保苞胞褒雹堡豹鲍暴爆剥薄瀑"},
    {"bei"  ,"被北倍杯备背卑悲碑贝狈钡惫焙辈"},
    {"ben"  ,"本奔苯笨夯"},
    {"beng" ,"崩绷甭迸蹦泵"},
    {"bi"   ,"必毕闭鼻比彼笔币避鄙逼庇毖陛毙敝痹蓖弊碧蔽壁臂"},
    {"bian" ,"边编鞭贬扁卞便变遍辨辩辫"},
    {"biao" ,"彪标膘表"},
    {"bie"  ,"憋鳖别瘪"},
    {"bin"  ,"宾斌彬滨鬓濒摈"},
    {"bing" ,"冰丙并病秉柄炳饼兵"},
    {"bo"   ,"拨波玻钵脖菠播伯驳帛泊勃铂舶博渤搏箔膊卜"},
    {"bu"   ,"不补哺捕布步怖部埠簿"},

    {NULL   ,NULL}
};
/*
********************************************************************
*函数名称 : PY_Search() 
*入口参数 : char *pPyMa         // 待查找的拼音码
*出口参数 : 无
*返回参数 : char *pTable        // 查找到的汉字码
*程序编写 : 戴晓天
*版    本 : V1.0.111124
*说    明 :
     在索引表中搜索待查找的拼音码,返回对应的汉字码起始地址。
********************************************************************
*/
char *PY_Search(char *pPyMa)
{
    char *pTable;
    int i;

    for ( i = 0; PY_Ma_Talbe[i]._PyMa!= NULL ;i++ )
    {
        if ( strcmp(pPyMa, PY_Ma_Talbe[i]._PyMa) == 0 )
        {
            break;
        }
    }

    // 注意此时如果没有找到,._PYTable为NULL
    pTable = PY_Ma_Talbe[i]._PYTable;  
    return pTable;
}
////////////////////////////////////////////////////////////////////////////////////////
// 主程序
int main(int argc, char* argv[])
{
    char chPYInput[10];     // 输入的拼音码
    char chPYIndex;         // 输入拼音码的索引
    char chInChar;          // 每次输入的字符
    char *PYString;         // 对应的汉字码表
    char chPYMaResult[2+1]; // 最后确定选择的汉字,中文占两个字节

    printf("请输入要查找的字符(只支持a、b开头):\n");
    memset(chPYInput,0,10);
    chPYIndex = 0;
    while (1)
    {
        chInChar = getch();

        // 如果是字母
        if ( chInChar >='a' && chInChar <= 'z' )         {             chPYInput[chPYIndex++] = chInChar;                         PYString = PY_Search(chPYInput);    // 这里输入要查找的字符             if ( PYString != NULL)             {                 printf(chPYInput);                 printf(PYString);                 printf("\n");             }             else             {                 printf("The PY code you input is not exist!\n");                 getch();                 return 0;             }         }         // 如果是退格键         else if ( chInChar == '\b' )         {             chPYInput[--chPYIndex] = 0;             PYString = PY_Search(chPYInput);    // 这里输入要查找的字符             if ( PYString != NULL)             {                 printf(chPYInput);                 printf(PYString);                 printf("\n");             }             else             {                 printf("The PY code you input is not exist!\n");                 getch();                 return 0;             }         }                 // 如果是数字键,则表示选择了对应的汉字         else if ( chInChar >= '0' && chInChar <= '9')         {             unsigned int index;             index = ( (chInChar-'0') * 2 ); // 汉字索引为偏差值*2,因为汉字一位是两个字节             // 如果还没有找到汉字表,或者索引超出了汉字表的最大索引,则什么都不做             if ( PYString == NULL ||  index > strlen(PYString)-1 )
            {
                ;
            }
            // 否则说明找到了正确的汉字,将其拷贝到内存变量中
            else
            {
                memcpy(chPYMaResult ,&PYString[index], 2);
                chPYMaResult[2] = '\0'; // 别忘了加上\0,否则打印时会出错
                break;
            }

        }

    }
   
    printf("你输入的字符是:%s\n", chPYMaResult);
    getch();

    return 0;
}

Cpp文件下载:

PYInput