白旭的博客欢迎您

既然选择了远方,便只顾风雨兼程!

18.1 C语言编程集锦

18.1.1 volatile的作用

volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

1)      编译器的优化

在本次线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后再取变量值时,就直接从寄存器中取值;

当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致

当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致

举个例子:

发薪资时,会计每次都把员工叫来登记他们的银行卡号;有一次,会计为了省事,没有即时登记,用了以前登记的银行卡号;刚好一个员工的银行卡丢了,已挂失该银行卡号;从而造成该员工领不到工资。

员工 -- 原始变量地址

银行卡号 -- 原始变量在寄存器的备份

18.1.2 #define DM9000_DBG(fmt,args...) printf(fmt, ##args)

标准C支持可变参数的函数,意味着函数的参数是不固定的,例如printf()函数的原型为:int printf( const char *format [, argument]... )

而在GNU C中,宏也可以接受可变数目的参数,例如:

#define DM9000_DBG(fmt,args...) printf(fmt, ##args)

这里args 表示其余的参数可以是零个或多个,这些参数以及参数之间的逗号构成arg 的值,在宏扩展时替换arg,例如下列代码:

DM9000_DBG("\n");

DM9000_DBG("NCR (0x00): %02x\n", DM9000_ior(0));

DM9000_DBG("rx status: 0x%04x rx len: %d\n", RxStatus, RxLen);

会被扩展为:

printf ("\n");

printf ("NCR   (0x00): %02x\n", DM9000_ior(0));

printf ("rx status: 0x%04x rx len: %d\n", RxStatus, RxLen);

18.1.3 字符转为数字

将ACSII转为数字需要-‘0’

#include“stdio.h”
int main(){
unsigned char c;
while(1)
{
    c = getchar();
    putchar(c);
    bank0 tacc_set(c - ‘0’);
}
return 0;
} 

18.1.4 inline函数功能

GUN的C关键字,在函数定义中函数返回类型前加上关键字inline,可以把函数指定为内联函数。关键字inline必须与函数定义放在一起才能使函数成为内联,仅仅将inline放在函数声明前面不起任何作用。inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。

在C&C++中,inline关键字用来定义一个类的内联函数,引入它的主要原因是用它替代C中表达式形式的宏定义。

表达式形式的宏定义:

cout<<shortString(s1,s2)<<endl;

在编译时展开为:

cout<<(s1.size() < s2.sizre() ? s1 : s2)<<endl;

inline优缺点:

1.inline定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换(像宏一样展开),没有了调用的开销,效率也很高。

2.由于将对函数的每一个调用都以函数本体替换之。所以会增加目标代码的大小。造成代码膨胀。这将导致程序体积太大,不利于在内存不大的机器上运行。

3. inline函数无法随着程序库的升级而升级。如果程序库中包含内联函数,一旦内联函数被改变,那么所有用到程序库的客户端程序都要重新编译。如果函数不是内联函数,一旦它有任何修改,客户端只需要重新连接就好。

18.1.5 sprintf函数功能

函数功能:把格式化的数据写入某个字符串

函数原型:int sprintf( char *buffer, const char *format [, argument] … );

返回值:字符串长度(strlen)

例子:

char* who = "I";

char* whom = "CSDN";

sprintf(s, "%s love %s.", who, whom); //产生:"I love CSDN. "  这字符串写到s中

sprintf(s, "%10.3f", 3.1415626); //产生:" 3.142"

NOTE:这样就不会报错了

u8 text[20];

sprintf((char *)text, "-Value:%d", Value);

18.1.6 关键字extern

用#include可以包含其他头文件中变量、函数的声明,为什么还要extern关键字?

1.头文件

其实头文件对计算机而言没什么作用,只是在预编译时在#include的地方展开一下,没别的意义了。将头文件的后缀改成xxx.txt,然后在引用该头文件的地方用#include"xxx.txt",编译、链接都很顺利的过去了,由此可知,头文件仅仅为阅读代码作用,没其他的作用了!

头文件就是对用户的说明。函数,参数,各种各样的接口的说明。

那既然是说明,那么头文件里面放的自然就是关于函数,变量,类的“声明”。

最好不要在头文件里定义什么东西。比如全局变量:

/*xx头文件*/

#ifndef _XX_头文件.H

#define _XX_头文件.H

int A;

#endif

那么,很糟糕的是,这里的int A是个全局变量的定义,所以如果这个头文件被多次引用的话,你的A会被重复定义,显然语法上错了。只不过有了这个#ifndef的条件编译,所以能保证你的头文件只被引用一次,不过也许还是不会出岔子,但若多个c文件包含这个头文件时还是会出错的,因为宏名有效范围仅限于本c源文件,所以在这多个c文件编译时是不会出错的,但在链接时就会报错,说你多处定义了同一个变量,

Linking...

incl2.obj : error LNK2005: "int glb" (?glb@@3HA) already defined in incl1.obj

Debug/incl.exe : fatal error LNK1169: one or more multiply defined symbols found

注意!!!

2.extern

对变量而言,如果你想在本源文件(例如文件名A)中使用另一个源文件(例如文件名B)的变量,方法有2种:

(1)在A文件中必须用extern声明在B文件中定义的变量(当然是全局变量);

(2)在A文件中添加B文件对应的头文件,当然这个头文件包含B文件中的变量声明,也即在这个头文件中必须用extern声明该变量,否则,该变量又被定义一次。

对函数而言,如果你想在本源文件(例如文件名A)中使用另一个源文件(例如文件名B)的函数,方法有2种:

(1)在A文件中用extern声明在B文件中定义的函数(其实,也可省略extern,只需在A文件中出现B文件定义函数原型即可);

(2)在A文件中添加B文件对应的头文件,当然这个头文件包含B文件中的函数原型,在头文件中函数可以不用加extern。

对上述总结换一种说法:

(a)对于一个文件中调用另一个文件的全局变量,因为全局变量一般定义在原文件.c中,我们不能用#include包含源文件而只能包含头文件,所以常用的方法是用extern  int a来声明外部变量。  

另外一种方法是可以是在a.c文件中定义了全局变量int global_num ,可以在对应的a.h头文件中写extern int global_num ,这样其他源文件可以通过include a.h来声明她是外部变量就可以了。

18.1.7 条件编译

18.1.7.1 #if 0…endif的用途

#if 0 ... #endif的作用和/*...*/的作用是一样的,就是注释!

可是注释为什么不用注释符号/*?

答:为了解决嵌套注释。如:

#include“stdio.h”
int main(){
  int a=11/*这是一个外层注释
  /* int *b=&a; //这是一个内层代码注释 *b = 10;
*/ a++; */ }

上面的程序编译后发现缺少了一个注释符号,因为注释符头 " / *  "是根据最近结束符 " */  "来判断注释的区域的,但是一但内嵌了就会发现错误。所以人们就使用了#if 0。如下:

#include“stdio.h”
int main(){
  int  a=11/*这是一个外层注释
#if  0  
  int *b=&a;  //这是一个内层代码注释
  *b = 10;
#endif
  a++;
*/
}

在有些地方很常见到它,而且少不了它。当你见识过系统级的源代码就焕然大悟了。就是用于系统裁剪。

系统裁剪是针对系统的用途,对系统的源代码进行一下优化,减少不必要的功能。

#include“stdio.h”
#define TEST_2 1
int main(){
  int a=11/*这是一个外层注释
#if  TEST_2
  int *b=&a;
  *b = 10;
#endif
  a++;
*/
}

如上面的例子,对于某些功能不需要,我们只需对于的功能TEST_2的宏定义改成0,然后重新编译就行了。当然一般宏定义是放在一个特定的.h文件,这样只需要更改那个文件所对应的值就行对系统进行裁剪而不需要关心具体代码。

18.1.7.2 #ifndef、#define、#endif的用途

看到很多程序的头文件写:

#ifndef HeaderName_h

#define HeaderName_h

//这里面通常写各种宏定义、其他头文件的包含

#endif

这样做的目的:防止该头文件被重复引用。

什么是“头文件被重复引用”?

答:其实“被重复引用”是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。

比如:存在a.h文件#include "c.h",而b.cpp文件同时#include "a.h" 和#include "c.h",此时就会造成c.h被b.cpp重复引用。

头文件被重复引用引起的后果:

有些头文件重复引用只是增加了编译工作的工作量,不会引起太大的问题,仅仅是编译效率低一些。但是对于大工程而言,编译效率低下那将是一件多么痛苦的事情。

而有些头文件重复包含,则会引起错误,比如:在头文件中定义了全局变量(虽然这种方式不被推荐,但确实是C规范允许的),这种头文件重复包含会引起全局变量的重复定义。

18.1.8 结构体定义即其常用初始化方法

首先定义一个结构体:

法1:

typedef struct TestInit
{
    char *name;
    int (*DeviceInit)(void);
} T_TestInit;

  在上面的代码中,实际上完成了两个操作:

  1、定义了一个新的结构类型,代码如下所示:

struct TestInit
{
    char *name;
    int (*DeviceInit)(void);
};

  其中,struct关键字和TestInit一起构成了这个结构类型,无论是否存在 typedef关键字,这个结构都存在。

  2、使用typedef为这个新的结构起了一个别名,叫T_TestInit,即:

  typedef struct TestInit T_TestInit;

  声明一个结构体是时用 T_TestInit test;

法2:

struct TestInit
{
    char *name;
    int (*DeviceInit)(void);
};

  声明一个结构体是时用 struct TestInit test;

初始化法1:定义时赋值

  特点:按成员定义顺序,从前到后逐个初始化。不初始化的默认为0或NULL。

  T_TestInit test = {"test",IICDeviceInit};

初始化法2:定义后逐个赋值

  T_TestInit test;

  test.name = "test";

  test.DeviceInit = IICDeviceInit;

初始化法3:定义后乱序赋值,最常用的方法

  特点:乱序的方式则很好的解决了按顺序初始化的问题,这种方式是按照成员名进行。

  T_TestInit test = {

    .name = "test",

    .DeviceInit = IICDeviceInit,

  }

18.1.9 使用结构体的时候"->"和"."区别

  定义的结构体如果是指针,访问成员时就用"->"

  如果定义的是结构体变量,访问成员时就用"."

  例如:

struct Test {
    int i;
    char c;
};
struct Test t; 访问成员就用:t.a;
struct AAA *t; 访问成员就用:t->a;

 

18.1.10 C库函数strtoul() 

  C 标准库 - <stdlib.h>

描述

  C 库函数 unsigned long int strtoul(const char *str, char **endptr, int base) 把参数 str 所指向的字符串根据给定的 base 转换为一个无符号长整数(类型为 unsigned long int 型),base 必须介于 2 和 36(包含)之间,或者是特殊值 0。

声明

  下面是 strtoul() 函数的声明。

  unsigned long int strtoul(const char *str, char **endptr, int base)

参数

  str -- 要转换为无符号长整数的字符串。

  endptr -- 对类型为 char* 的对象的引用,其值由函数设置为 str 中数值后的下一个字符。

  base -- 基数,必须介于 2 和 36(包含)之间,或者是特殊值 0。

返回值

  该函数返回转换后的长整数,如果没有执行有效的转换,则返回一个零值。

实例

  下面的实例演示了 strtoul() 函数的用法。

#include <stdio.h>
#include <stdlib.h>

int main()
{
   char str[30] = "2030300 This is test";
   char *ptr;
   long ret;
   ret = strtoul(str, &ptr, 10);
   printf("数字(无符号长整数)是 %lu\n", ret);
   printf("字符串部分是 |%s|", ptr);
   return(0);
}

以下结果:

  数字(无符号长整数)是 2030300

  字符串部分是 | This is test|

18.1.11 函数指针  

  函数指针的声明形式:void (*pFunction)()

  当然,没有参数的情况下也可写成void (*pFunction)(void)的形式。那么pFunction函数指针的原型就是void (*)(void),即把变量名去掉。

  因此,对于一个给定的entry地址,要把它转换成为函数指针,就是

  (void (*) (void))entry;

  对于函数指针的调用,ANSI C认为pFunction()*pFunction()都是正确的,所以((void (*) (void))(entry))();就形成一个函数调用。

  如:

void *pVoid;
T_InputEvent tInputEvent;
/*定义函数指针 */

int(*GetInputEvent)(PT_InputEvent ptInputEvent);
GetInputEvent= (int (*)(PT_InputEvent))pVoid; //强制转换为函数指针
GetInputEvent(&tInputEvent);//函数调用

18.1.12 结构体占用空间大小计算

  例题如下:在32位机器上,下面的代码中:

struct {
    int i;   
    union U
    {
        char buff[13];
        int i;
    }u;       
    enum{red , green, blue}color;
}a;

  sizeof(a)的值是多少?如果在代码前面加上#pragma pack(2)呢?

  之前一直认为都是按4字节对齐的方式,但是#pragma pack(2)是什么意思呢?

  数据占用内存的大小取决于数据本身的大小和其字节对齐方式,所谓对齐方式即数据在内存中存储地址的起始偏移应该满足的一个条件。各种基本数据类型的数据该怎么对齐呢?在32位系统中,列出下列表格总结:

基本数据类型

占用内存大小

首地址偏移

double/long long

8

8

int/long

4

4

float

4

4

short

2

2

char

1

1

  其中,字节对齐方式(首地址偏移),表示的是该类型的数据的首地址,应是该类型的字节数的倍数。这是在默认的情况下,即不加#pragma pack(n),如果用#pragma pack(n)重定义了字节对齐方式,按照数据类型得到的对齐方式比n的倍数大,那就按照n的倍数指定的方式来对齐;如果按照数据类型得到的对齐方式比n小,那就按照默认的方式来对齐。听不懂没关系,一起看看下列的例子

  对齐规则:

  1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset0的地方,以后每个数据成员的对齐按照#pragma pack(n),中n指定的数值和这个数据成员自身长度中,比较小的那个进行。

  2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack(n)指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

  结合12推断:当#pragma packn值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。

  上面只是基本数据类型,比较简单,一般复杂的组合数据类型,比如enum(枚举)、Union(联合)、struct(结构体)、class(类)。

  数组,数组是第一个元素对齐,以后的各个元素就对齐了。

  enum,枚举类型,一般来说大小为4字节,因为4个字节能够枚举4294967296个变量,大小足够了。

  Union,联合类型。联合类型的大小是最长的分量的长度,加上补齐的字节。这里容易有一个谬误,有人说补齐的字节是将联合类型的长度补齐为各分量基本类型的倍数,这个说法在默认的字节对齐(4字节或8字节)中没问题,但是当修改对齐方式之后就有问题了。先看一下默认的情况。

union t{ 
    char buff[13];//0+13+3
    int i; 
}t;

  上述定义的联合体,在默认的字节对齐方式中,大小为16字节。首先计算得到联合最长的分量长度是sizeof(char)*13=13字节。但是13不是sizeof(int)的倍数,所以将13扩充至16,最终得到sizeof(t)=16字节。

  这是在默认情况下,扩充后的大小是各分量基本类型大小的倍数。但是,如果指定对齐 方式为#pragma pack(2),那情况就不一样了。此时得到的最长分量还是13字节,不过扩充时不是按照4字节的倍数来算,而是按照2的倍数(pragma pack指定的)来算。最终得到大小为14字节。

  Union联合体比较简单,不牵涉到各分量的起始偏移地址对齐的问题。

  需要注意的是struct中定义的成员函数占整体的空间,因为是函数指针所以占用4字节。struct占用内存大小的计算有两点,第一点是各个分量的偏移地址的计算,第二点是最终整体大小要进行字节对齐。

struct{ 
    char a[15]; //占15字节,从0偏移,下面的int是从15开始偏移 
    int x;//偏移量 15+1=16+4
}s1;//结果为20字节 

struct{ 
    char a[15]; // 
    int x; //偏移量 16字节 
    char b; //偏移量 21字节 
}s2; //结果为21字节,按最大基本类型对齐,补充到24字节 

struct{ 
    char a[15]; 
    int x;  //偏移量 16字节 
    double b; //偏移量 24字节 
}s3;//结果为32字节

  以s3为例。首先,从偏移量为0的地方开始放char,连续放15个,每个占1字节。则int x对应的偏移量是第15个字节,按照上面表格的说明,int类型的偏移量应该能够整除int类型的大小,所以编译器填充1个字节,使int x从第16个字节开始放置。x占4个字节,所以double b的偏移量是第20个字节,同理,20不能整除8(double类型的大小),所以编译器填充4字节到第24个字节,即double b从第24个字节开始放置。最终结果为15+1+4+4+8=32字节。

  上面这个例子还不够明显,再举一个需要最后补充字节的例子。

struct{ 
    char a[15]; 
    int x;  //偏移量 16字节 
    double b; //偏移量 24字节 
    char c;//偏移量 32字节 
}s3;//共33字节,按最大基本类型对齐,补充到40字节(整除8) 

  上面的例子中,最后多了一个char型数据。导致最后得出的大小是33字节,这个大小不能够整除结构体中基本数据类型最大的double,所以要按能整除sizeof(double)来补齐,最终得到40字节。

  计算struct这种结构体的大小,都分两步:

  第一,各个分量的偏移;

  第二,最后的补齐。

  如果主动设定对齐方式会如何:

#pragma pack(2) 
    struct{ 
        char a[13]; //占13字节,从0开始偏移,下面的int从13开始偏移 
        int x;//偏移量13+2=14,不按整除4来偏移,按整除2来偏移 
    }s4;//14+4=18 

    struct{ 
        char a[13]; // 
        int x; //偏移量 14字节 
        char b; //偏移量 18字节 
    }s5; //结果为19字节,按2字节对齐,补充到20字节 

struct{ 
        char a[13]; 
        int x;  //偏移量 14字节 
        double b; //偏移量 18字节 
        char c;//偏移量 26字节 
    }s6;//共27字节,按2字节对齐,补充到28字节(整除8) 
  如果#pragma pack(1),那就没有对齐了,直接将各个分量相加就是结构体的大小了。
  上面的分析,可以应付enum、union、struct(或class)各种单独出现的情况了。下面再看看组合的情况。
struct ss0{ 
    char a[15]; //占15字节,从0开始偏移,下面的int是从15开始偏移 
    int x;//偏移量15+1=16
}s1;//结果为20字节

struct ss1   
{ 
    char a[15]; // 
    int x; //偏移量 16字节 
    char b; //偏移量 21字节 
}s2; //结果为21字节,按最大基本类型对齐,补充到24字节 

struct  ss2 
{ 
    char a[15]; 
    int x;  //偏移量 16字节 
    double b; //偏移量 24字节 
    char c;//偏移量 32字节 
}s3;//共33字节,按最大基本类型对齐,补充到40字节(整除8) 

struct 
{ 
    char a; //偏移0,1字节 
    struct ss0 b;//偏移1+3=4,20字节(结构体中最大成员为int) 
    char f;//偏移24, 1字节 
    struct ss1 c;//偏移25+3,24字节(结构体中最大成员为int)
    char g;//偏移52,1字节 
    struct ss2 d;//偏移53+3,40字节(结构体中最大成员为double)
    char e;//偏移96,1字节 
}s7;//共97字节,不能整除sizeof(double),所以补充到104字节 

  首先,作为成员变量的结构体的偏移量必须是自己最大成员类型字节长度的整数倍。其次,整体的大小应该是结构体中最大基本类型成员的整数倍。结构体中字节数最大的基本数据类型,包括内部结构体的成员变量。

  根据这些原则,分析一下上面的结果。第一个struct ss0 b的大小之前已经算过,是20字节,其偏移量是1字节,因为strut ss0中最大的数据类型是int类型,故而strut ss0的偏移量应该能够整除sizeof(int)=4,所以偏移量为4。同理,可得strut ss1。然后是strut ss2,其偏移量是53字节,但是strut ss2最大的成员变量的double类型,故而其偏移量应该能够整除sizeof(double),补充为56字节。最后得到97字节的结构体,而 struct s7 最大的成员变量是struct ss2中的double,所以struct s7应该按8字节对齐,故补充到能够整除8的104,所以结果就是104字节。

  如果将struct s7用#pragma pack(2)包围起来,其他的不变,可以推测,结果将是92字节,因为其内部各结构体成员也都不按自己内部最大的数据类型来偏移。代码如下,经测试,结果是正确的。

struct A 
{ 
    int i;  //偏移0,4字节 
    union U  //4+4+16
    { 
        char buff[13]; //13+3
        double i; 
    }u; //偏移4,不能整除sizeof(double),所以偏移需要补充到8,大小16
    char d;//偏移24,大小1字节 
    enum{red , green, blue}color;//偏移25,补充到28,大小4字节  
    char e;//偏移32,大小1字节 
}a;//大小33字节,不能整除sizeof(double),补充到40字节 

  上面的例子中,上面的例子既有内部偏移的对齐,又有最后的补齐。可见struct A补齐时需要对齐的是union U u的成员double i,所以最后是补充到了40字节。


 

posted on 2019-01-13 15:51  小学生_白旭  阅读(368)  评论(0编辑  收藏  举报

导航