C编程笔记(一)

12 篇文章 0 订阅

本文记录《高质量嵌入式Linux C编程》等书中的一些易错内容。

 

一、字符串常量与字符数组

字符串常量放在代码区,是只读不可写的(所以字符串常量不可修改)。

字符数组与普通变量一样存放在栈上,可以进行修改。

如果尝试修改字符串常量,编译时无问题,运行时会出现段错误。

int test2()
{
  char *str = "hello world";  

  //*str = "goodbye world";  //尝试修改字符串常量,编译无问题,运行时报段错误。Program terminated with signal 11, Segmentation fault.  Program received signal SIGSEGV, Segmentation fault

}

 

可以看到p2[7]= "message"或char p4[7]= {'m','e','s','s','a','g','e'} 定义数组,然后通过printf("p2 = %s,",p2)打印字符数组,会一直打印到字符串结束‘\0’为止,如果p2[7]或者p4[7]不为‘\0’则会继续打印输出。所以字符数组的长度一定需要注意长度比字符串多1,否则可能有意想不到的错误。  对于p2[7] = 'a';  p4[7] = 'b';进行超出数组范围的修改也会造成意想不到的错误。

对于sizeof(p1)得到的是指针的size为8字节,sizeof(p2)得到是数组占用的字节数为7。

test6()
{
  char *p1 = "message";
  char p2[7]= "message";  
  char p3[]= "message";  
  char p4[7]= {'m','e','s','s','a','g','e'} ;
  printf("test6 :  p1 = %s,",p1); 
  printf("p2 = %s,",p2); 
  printf("p3 = %s,",p3); 
  printf("p4 = %s\n",p4); 

  p2[7] = 'a';
  p4[7] = 'b';
  printf("test6 : p2 = %s,",p2); 
  printf("p4 = %s\n",p4);

  printf("test6 : sizeof p1 = %d,",sizeof(p1));
  printf("sizeof p2 = %d,",sizeof(p2));
  printf("sizeof p3 = %d,",sizeof(p3));
  printf("sizeof p4 = %d\n",sizeof(p4));

  strcpy(p2, p1);
  //p1[3] = '\0';  //Program received signal SIGSEGV, Segmentation fault.
  p2[3] = '\0';
  p3[3] = '\0';
  p4[3] = '\0';
  printf("test6 : p1 = %s,",p1); 
  printf("p2 = %s,",p2); 
  printf("p3 = %s,",p3); 
  printf("p4 = %s\n",p4); 
  printf("test6 : sizeof p1 = %d",sizeof(p1));
  printf("sizeof p2 = %d,",sizeof(p2));
  printf("sizeof p3 = %d,",sizeof(p3));
  printf("sizeof p4 = %d\n",sizeof(p4));

}

test6 :  p1 = message,p2 = message,p3 = message,p4 = message
test6 : p2 = messagea�@,p4 = messageb �P��
test6 : sizeof p1 = 8,sizeof p2 = 7,sizeof p3 = 8,sizeof p4 = 7
test6 : p1 = message,p2 = mes,p3 = mes,p4 = mes
test6 : sizeof p1 = 8sizeof p2 = 7,sizeof p3 = 8,sizeof p4 = 7

 

二、常量指针和指针常量

 

const char * p1

p1 所指向的对象是只读的

char const * p2

p2所指向的对象是只读的

char * const p3

p3指针是只读的

const char * const p4

p4指针和p4 所指向的对象都是只读的

char const * const p5

p5指针和p5所指向的对象都是只读的

typedef char *p_str;

const p_str p6 = str;

p6指针是只读的

typedef char *p_str;

p_str const p7 = str;

p7指针是只读的

test5()
{
  typedef char *p_str;
  char str[] = "abc";

  const char *p1 = str;  
  char const *p2 = str;
  char *const p3 = str;
  const p_str p4 = str;  //not "const char *p2", "char * const p2;
  p_str const p5 = str;  

  //p1[0] = 'd';  //assignment of read-only location ‘*p1’
  //p2[0] = 'e';  //assignment of read-only location ‘*p2’
  p3[0] = 'f';
  p4[0] = 'g';
  p5[0] = 'h';

  p1++;
  p2++;  	
  //p3++;       //increment of read-only variable ‘p3’
  //p4++;       //increment of read-only variable ‘p4’
  //p5++;       //increment of read-only variable ‘p5’
  printf("test5 :  p1 = %c, p2 = %c, p3 = %c, p4 = %c, p5 = %c\n",*p1, *p2, *p3, *p4, *p5); 

}

test5 :  p1 = b, p2 = b, p3 = h, p4 = h, p5 = h

 

 

三、static与extern

static

1、修饰局部变量,其生命周期和存储空间发生变化,但是其作用域并没有改变,仍然是一个局部变量。一般局部变量放在栈区,生命周期在语句块执行结束时便结束了。但是用static进行修饰后,该变量便存放在静态数据区,其生命周期一直持续到整个程序执行结束为止。

2、修饰全局变量,则该变量只能在本源文件中可见。

3、修饰函数,则该函数只能在本源文件中可见。

 

extern

1、extern变量名,声明变量。

2、extern函数名,由于函数默认是外部的,所以函数名是否有extern作用范围一致,均是外部的。

test3()
{
  static int test_times = 1;
  int value = 10;
  printf("test3 test %d times, value = %d\n", test_times, value);
  test_times ++;
  value+=;
}

void main()
{

  test3();
  test3();
  test3();

}



test3 test 1 times, value = 10
test3 test 2 times, value = 10
test3 test 3 times, value = 10

四、typedef用法

1、typedef与结构

如下定义时,编译器报错“error: unknown type name ‘p_node’”,新结构建立的过程中,遇到了p_next域的声明,类型是p_node,而p_node是该结构类型的新名字,那么在类型本身还没有建立完成时,这个类型的新名字还不存在,因此在结构定义里面的域暂时还不能使用类型的新名字p_node。

typedef struct tag_node
{
  char *p_item;
  p_node p_next;  //error: unknown type name ‘p_node’

}*p_node;

解决这个问题的方法:

/* method 1*/
typedef struct tag_node
{
  char *p_item;
  struct tag_node *p_next;  
}*p_node;


/* method 2*/
typedef struct tag_node *pNode;
typedef struct tag_node
{
  char *p_item;
  pNode p_next;
}*p_node;

/* method 3*/
struct tag_node
{
  char *p_item;
  struct tag_node *p_next;
};
typedef struct tat_node *p_node;

2、typedef与复杂的变量声明

对复杂变量建立一个类型别名的方法:在传统的变量声明表达式里面用类型名替代变量名,然后把关键字加在该语句的开头。

//1、int *(*a[5])(int, char*);  函数指针数组,返回类型为int *,参数为(int, char*)。
typedef int *(*pfun)(int, char*);
pfun a[5];

//2、void (*b[10])(void (*)());  函数指针数组,返回类型为void。参数为函数指针(*)(),参数指向的函数参数为空。
typedef void (*p_fun_param)();
typedef void (*p_fun)(p_fun_param);
p_fun b[10];

//3、double (*)() (*pa)[9];  pa为二维数组指针,第二维长度为9,数组中元素为函数指针double (*)(),函数返回值为double,参数为空。
typedef double (*p_fun)();
typedef p_fun (*p_fun_param)[9];
p_fun_param pa;

五、指针

1、指针的类型和指针所指向的类型

  1. 从语法的角度,只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。
  2. 当你通过指针访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当作什么来看待。从语法的角度,只需把指针声明语句里的指针名字和名字左边的指针声明符“*”去掉,剩下的就是指针所指向的类型。

在指针的算数运算中,指针所指向的类型有很大的作用。

指针所指向的内存区,就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。

 

2、void指针

在C语言中,任何时候都能用其他类型的指针来代替void指针,或者用void指针来代替其他类型的指针,并且不需要进行强制转换。

3、指针函数与函数指针

指针函数

返回值为指针的函数,定义为“数据类型 *fun(参数列表);”,如“char *fun(int*,int*);”,即返回值为char *类型。

当函数返回值为指针类型时,应该尽量不要返回局部变量的指针,因为局部变量是定义在函数内部,当这个函数调用结束了,局部变量的栈内存也被释放了,因此不能够正确的得到返回值。实际上,内存已经被释放了,这个地址已经是无效的了,但这个指针的地址通过函数返回值返回过去了,因此对这个指针的使用是很危险的。

 

函数指针

指向函数的指针,定义为“数据类型 (*fun)(参数列表);”,()的优先级比*高,所以*fun加括号,例如“void (*fun)(int*, int*);”

在C语言中,变量有它的地址,同理函数也是有地址的,那么把函数的地址赋给函数指针,再通过函数指针调用这个函数就可以了。

第一步:定义函数指针

第二步:定义函数

第三步:把函数的地址赋给函数指针

第四步,通过函数指针去调用这个函数

int *funptest1(int *a )
{
  /*虽然本例执行没有问题,但是当函数返回值为指针类型时,应该尽量不要返回局部变量的指针,因为局部变量是定义在函数内部,当这个函数调用结束了,局部变量的栈内存也被释放了,因此不能够正确的得到返回值。实际上,内存已经被释放了,这个地址已经是无效的了,但这个指针的地址通过函数返回值返回过去了,因此对这个指针的使用是很危险的。*/
  int b = 10;  
  return &b;
}

int *funptest2(int *a)
{
  *a = 10;  
  return a;
}

test8()
{
  int val = 100;
  int *pint;
  pint = funptest1(&val);
  printf("test8 : funtest1 val = %d, &val = %p, *pint = %d, pint = %p\n", val, &val, *pint, pint);
  
  pint = funptest2(&val);
  printf("test8 : funtest1 val = %d, &val = %p, *pint = %d, pint = %p\n", val, &val, *pint, pint);
  
}

test8 : funtest1 val = 100, &val = 0x7fff4a77b624, *pint = 10, pint = 0x7fff4a77b60c
test8 : funtest1 val = 10, &val = 0x7fff4a77b624, *pint = 10, pint = 0x7fff4a77b624

 

六、数组指针

 

test9()
{

  typedef int (*pInt5)[5];
  pInt5 p1;
  int *p2;
  int num[2][5] = {{1,2,3,4,5}, {6,7,8,9,10}};
  p1 = num;
  p2 = num[0];
  printf("test9 : sizeof(num) = %d, sizeof(p1) = %d, sizeof(*p1) = %d\n",sizeof(num), sizeof(p1), sizeof(*p1));
  printf("test9 : p1 = %p, p2= %p, p1 val = %d, p2 val = %d\n", p1, p2, **p1, *p2);
  p1++ ;
  p2++ ;
  printf("test9 : p1 = %p, p2= %p, p1 val = %d, p2 val = %d\n", p1, p2, **p1, *p2);

}  

test9 : sizeof(num) = 40, sizeof(p1) = 8, sizeof(*p1) = 20
test9 : p1 = 0x7fffee7cda20, p2= 0x7fffee7cda20, p1 val = 1, p2 val = 1
test9 : p1 = 0x7fffee7cda34, p2= 0x7fffee7cda24, p1 val = 6, p2 val = 2

 

七、位域

位域的定义和位域变量的说明

struct 位域结构名

{

 位域列表

};

其中位域列表的形式为:

type [member_name] : width ;

 

1) 位域类型只能为 int(整型),unsigned int(无符号整型),signed int(有符号整型) 三种类型,决定了如何解释位域的值。

2)位域中位的数量。宽度必须小于或等于指定类型的位宽度。比如在VCint是占4个字节,那么宽度最多只能是32位。

3)无名位域不能被访问,但是会占据空间。

4)不能对位域进行取地址操作。

5)若位域占据的二进制位数为0,则这个位域必须是无名位域,下一个位域从下一个位域存储单元开始存放。

6)若位域出现在表达式中,则会自动进行整型升级,自动转换为int或者unsigned int

7)对位域赋值时,不要超过位域所能表示的最大范围,否则可能会造成意想不到的结果。

8)位域不能出现数组的形式。

 

一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。也可以有意使某位域从下一单元开始。

如果一个位域存储单元能够存储在位域结构中的所有成员,那么位域结构中的所有成员只能放在一个位域存储单元中,不能放在两个位域存储单元中;如果一个位域存储单元不能容纳位域结构中的所有成员,那么剩余的位域从下一个位域存储单元开始存放(在VC中位域存储单元的大小是4字节)。

如果一个位域结构中只有一个占有0位的无名位域,则只占1或0字节的空间(C语言中是占用0字节,而C++中占1字节);否则其它任何情况下,一个位域结构所占的空间至少是一个位域存储单元的大小。

#define MAX_INT 0xffff

struct bit_node1
{
  unsigned int a:5;
  unsigned int :4;
  unsigned int b:4;
  int c:32;
  int :6;
  
};

struct bit_node2
{
  unsigned int a:4;
  unsigned int :4;
  unsigned int b:4;
  int c:6;
};

struct bit_node3
{
  unsigned int a:4;
  unsigned int :0;
  unsigned int b:4;
  int c:6;
};

struct ONE_BYTE
{
    unsigned char _bool : 1;
    unsigned char del_flag : 1;
    unsigned char status : 4;
} one_byte;

struct TWO_BYTE
{
    unsigned char ccc1 : 4;
    unsigned char ccc2 : 4;
    unsigned char ccc3 : 4;
    unsigned char ccc4 : 4;
} two_byte;

struct THREE_BYTE
{
    unsigned char ccc1 : 4;
    unsigned char ccc2 : 4;
    unsigned char ccc3 : 4;
    unsigned char ccc4 : 4;
    unsigned char ccc5 : 4;
} three_byte;

struct FOUR_BYTE
{
    unsigned int ccc1 : 16;
    unsigned int ccc2 : 16;
} four_byte;


struct EIGHT_BYTE
{
    unsigned char ccc1 : 1;
    unsigned int ccc2 : 1;
} eight_byte;
test10()
{
  struct bit_node1 node1 = {0};
  struct bit_node2 node2 = {0};
  struct bit_node3 node3 = {0};
  unsigned char *pnode;
  int idx;
  printf("test10 : sizeof(node1) = %d, sizeof(node2) = %d, sizeof(node3) = %d,\n", sizeof(node1), sizeof(node2), sizeof(node3));

  node1.a = MAX_INT;
  node1.b = MAX_INT;
  node1.c = MAX_INT;

  node2.a = MAX_INT;
  node2.b = MAX_INT;
  node2.c = MAX_INT;

  node3.a = MAX_INT;
  node3.b = MAX_INT;
  node3.c = MAX_INT;

  pnode = (char *)(&node1);
  printf("node1 byte:\n");
  for(idx = 0; idx < sizeof(node1); idx++)
  { 
    printf("byte %d: %x, ", idx, *(pnode+idx));
  }
  
  printf("\nnode2 byte:\n");
  pnode = (char *)(&node2);
  for(idx = 0; idx < sizeof(node2); idx++)
  { 
    printf("byte %d: %x, ", idx, *(pnode+idx));
  }
  
  printf("\nnode3 byte:\n");
  pnode = (char *)(&node3);
  for(idx = 0; idx < sizeof(node3); idx++)
  { 
    printf("byte %d: %x, ", idx, *(pnode+idx));
  }
  printf("\n");
  printf("sizeof one_byte is : %lu\n", sizeof(one_byte));
  printf("sizeof two_byte is : %lu\n", sizeof(two_byte));
  printf("sizeof three_byte is : %lu\n", sizeof(three_byte));
  printf("sizeof four_byte is : %lu\n", sizeof(four_byte));
  printf("sizeof eight_byte is : %lu\n", sizeof(eight_byte));
  
}

node1 byte:
byte 0: 1f, byte 1: 1e, byte 2: 0, byte 3: 0, byte 4: ff, byte 5: ff, byte 6: 0, byte 7: 0, byte 8: 0, byte 9: 0, byte 10: 0, byte 11: 0, 
node2 byte:
byte 0: f, byte 1: ff, byte 2: 3, byte 3: 0, 
node3 byte:
byte 0: f, byte 1: 0, byte 2: 0, byte 3: 0, byte 4: ff, byte 5: 3, byte 6: 0, byte 7: 0, 
sizeof one_byte is : 1
sizeof two_byte is : 2
sizeof three_byte is : 3
sizeof four_byte is : 4
sizeof eight_byte is : 4

 

八、内存对齐

1、结构体内存对齐

结构体内存分配原则:

  • 原则一:结构体中元素按照定义顺序存放到内存中,但并不是紧密排列。从结构体存储的首地址开始 ,每一个元素存入内存中时,它都会认为内存是以自己的宽度来划分空间的,因此元素存放的位置一定会在自己大小的整数倍上开始。
  • 原则二: 在原则一的基础上,检查计算出的存储单元是否为所有元素中最宽的元素长度的整数倍。若是,则结束;否则,将其补齐为它的整数倍。

数据类型自身对齐值:基本数据类型的自身对齐值,等于sizeof(基本数据类型)。

指定对齐值:“pragma pack(value)”时的指定对齐值。

结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。

数据成员、结构体和类的有效对齐值N:自身对齐值和指定对齐值中较小的那个值。

有效对齐值N是最终用来决定数据存放地址方式的值,也即“数据存放起始地址”%N=0。

内存对齐原则:

第一条:第一个成员的首地址%N为0
第二条:每个成员的首地址是自身大小的整数倍
       第二条补充:以4字节对齐为例,如果自身大小大于4字节,都以4字节整数倍为基准对齐。
第三条:最后以结构总体对齐。
        第三条补充:以4字节对齐为例,取结构体中最大成员类型倍数,如果超过4字节,都以4字节整数倍为基准对齐。(其中这一条还有个名字叫:“补齐”,补齐的目的就是多个结构变量挨着摆放的时候也满足对齐的要求。)

可通过#pragma pack(n)指定对齐值,不指定时默认对齐值为8。 

#define FPOS( type, field ) ( (unsigned long int) &(( type *) 0)-> field )  //得到一个field在结构体(struct)中的偏移量

#define FSIZ( type, field ) sizeof( ((type *) 0)->field )  //得到一个结构体中field所占用的字节数

#pragma pack(4) 
typedef struct MemAlign  
{  
    char a[18];   //18 + 2pad
    double b;     //8
    char c;       //1 + 3pad
    int d;        //4
    short e;      //2 + 2pad
}MemAlign;  

typedef struct MemAlign2  
{  
    char a;  //1+3pad
    double b;  //8
    int c;  //4
    short d;  //2+2pad
    float e;  //8
    int *f;  //8
    char *g;  //8        
}MemAlign2;  

test11()
{
  printf("sizeof  MemAlign is : %d\n", sizeof(MemAlign));
  printf("field size %d, %d, %d %d, %d\n",FSIZ(MemAlign, a), FSIZ(MemAlign, b), FSIZ(MemAlign, c), FSIZ(MemAlign, d), FSIZ(MemAlign, e));
  printf("field offset %d, %d, %d %d, %d\n", FPOS(MemAlign, a),FPOS(MemAlign, b),FPOS(MemAlign, c),FPOS(MemAlign, d),FPOS(MemAlign, e));
  printf("sizeof  MemAlign2 is : %d\n", sizeof(MemAlign2));
  printf("field size %d, %d, %d %d, %d, %d, %d\n",FSIZ(MemAlign2, a), FSIZ(MemAlign2, b), FSIZ(MemAlign2, c), FSIZ(MemAlign2, d), FSIZ(MemAlign2, e), FSIZ(MemAlign2, f), FSIZ(MemAlign2, g));
  printf("field offset %d, %d, %d %d, %d, %d, %d\n", FPOS(MemAlign2, a),FPOS(MemAlign2, b),FPOS(MemAlign2, c),FPOS(MemAlign2, d),FPOS(MemAlign2, e),FPOS(MemAlign2, f), FPOS(MemAlign2, g));


}

sizeof  MemAlign is : 40
field size 18, 8, 1 4, 2
field offset 0, 20, 28 32, 36
sizeof  MemAlign2 is : 40
field size 1, 8, 4 2, 4, 8, 8
field offset 0, 4, 12 16, 20, 24, 32

2、pragma pack语法

用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题,有时候为了内存对齐需要补齐空字节。通常写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。

语法:#pragma pack( [show] | [push | pop] [, identifier], n )
作用:指定结构,联合和类的包对齐方式(pack alignment)
show(optical):显示当前packing aligment的字节数,以warning message的形式显示。
push(optical):
Pushes the current packing alignment value on the internal compiler stack, and sets the current packing alignment value to n. If n is not specified, the current packing alignment value is pushed.
pop(optical):
Removes the record from the top of the internal compiler stack. If n is not specified with pop, then the packing value associated with the resulting record on the top of the stack is the new packing alignment value. If n is specified, for example, #pragma pack(pop, 16), n becomes the new packing alignment value. If you pop with identifier, for example, #pragma pack(pop, r1), then all records on the stack are popped until the record that hasidentifier is found. That record is popped and the packing value associated with the resulting record on the top of is the stack the new packing alignment value. If you pop with an identifier that is not found in any record on the stack, then the pop is ignored.
identifier(optional):
When used with push, assigns a name to the record on the internal compiler stack. When used with pop, pops records off the internal stack until identifieris removed; if identifier is not found on the internal stack, nothing is popped.
n (optional):
Specifies the value, in bytes, to be used for packing. If the compiler option /Zp is not set for the module, the default value for n is 8. Valid values are 1, 2, 4, 8, and 16. The alignment of a member will be on a boundary that is either a multiple of n or a multiple of the size of the member, whichever is smaller.

#pragma pack规定的对齐长度:

结构,联合,或者类的数据成员,每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。也就是说,当#pragma pack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。而结构整体的对齐,则按照结构体中最大的数据成员 #pragma pack指定值之间,较小的那个进行。

注意:文件使用#pragma pack(n) 改变了缺省设置而不恢复,通常可以使用#pragma pack(push, n)和#pragma pack(pop)进行设置与恢复。

 

3、malloc内存分配

对齐参数(MALLOC_ALIGNMENT)大小的设定需要满足以下两点:

  1. 必须是2的幂

  2. 必须是void *的整数倍

所以从request2size可知,在64位系统,如果申请内存为1~24字节,系统内存消耗32字节(包括指针8字节),当申请25字节的内存时,系统内存消耗48字节。而对于32位系统,申请内存为1~12字节时,系统内存消耗为16字节(包括指针4字节),当申请内存为13字节时,系统内存消耗为24字节。

test7()
{

  char a[] = "hello123";

  char *p1 = (char *)malloc(sizeof(char)*strlen(a));
  char *p2 = (char *)malloc(sizeof(char)*(strlen(a)+1));

  printf("test7 : strlen(a) = %d, malloc_usable_size(p1) = %d, (p2) = %d\n",strlen(a), malloc_usable_size(p1),malloc_usable_size(p2)); 
  free(p1)
  free(p2);

}

test7 : strlen(a) = 8, malloc_usable_size(p1) = 24, (p2) = 24

九、如何编写有多个返回值的C语言函数

1、利用全局变量

2、传递数组指针

3、传递结构体指针

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值