C语言基础-复合类型数据,编译相关,关键字,位运算,内存管理,gdb调试
结构体
结构体内存对齐模式
例如int类型占用4个字节,地址只能在0,4,8等位置上。
例如:
假设操作系统的默认对齐系数是4,那么对与long long这个类型的变量就不满足第一节所说的,也就是说long long这种结构,可以存储在被4整除的位置上,也可以存储在被8整除的位置上。
可以通过#pragma pack()语句修改操作系统的默认对齐系数,编写程序的时候不建议修改默认对齐系数,
例如假设没有内存对齐(默认对齐系数为8),结构体xx的变量位置会出现如下情况:
struct xx{
char b; //0xffbff5e8
int a; //0xffbff5e9
int c; //0xffbff5ed
char d; //0xffbff5f1
};
操作系统先读取0xffbff5e8-0xffbff5ef的内存,然后在读取0xffbff5f0-0xffbff5f8的内存,为了获得值c,就需要将两组内存合并,进行整合,这样严重降低了内存的访问效率。(这就涉及到了老生常谈的问题,空间和效率哪个更重要?)。
这样大家就能理解为什么结构体的第一个变量,不管类型如何,都是能被8整除的吧(因为访问内存是从8的整数倍开始的,为了增加读取的效率)!
在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域:
struct bs{
unsigned m;
unsigned n: 4;
unsigned char ch: 6;
};
struct bs{
unsigned m: 6;
unsigned n: 12;
unsigned p: 4;
};
printf("%d\n", sizeof(struct bs));
struct bs
{
unsigned m: 12;
unsigned char ch: 4;
unsigned p: 4;
};
printf("%d\n", sizeof(struct bs));
struct bs
{
unsigned m: 12;
unsigned char ch;
unsigned p: 4;
};
printf("%d\n", sizeof(struct bs));
struct bs{
int m: 12;
int : 20; //该位域成员不能使用
int n: 4;
};
无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。
上面的例子中,如果没有位宽为 20 的无名成员,m、n 将会挨着存储,sizeof(struct bs) 的结果为 4;有了这 20 位作为填充,m、n 将分开存储,sizeof(struct bs) 的结果为 8。
{
成员列表
};
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
共用体的所有成员起始地址的是一样的。
int checkLittleEndian()
{
union check ck;
ck.i = 1;
if (ck.ch == 1)
{
return 1;
}
return 0;
}
enum typeName{ valueName1, valueName2, valueName3, ...... };
enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun };
1) 枚举列表中的 Mon、Tues、Wed 这些标识符的作用范围是全局的,不能再定义与它们名字相同的变量。
2) Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量
预编译相关
2、从#define宏定义位置开始,以后的代码就都可以使用这个宏了
3、编译器会在预处理的时候用真身替换宏
数值宏常量
#define PI 3.1415926
#define ERROR -1
字符串宏常量
#define STR_1 "hello"
#define STR_2 hello
#define STR_3 "hell\
o"
反斜杠作为接续符的时候,在本行后面不能再有任何字符,空格都不行。
#define MIN(a, b) ((a)<(b) ? (a) : (b))
#undef PI
__LINE__:表示正在编译的文件的行号
__FILE__:表示正在编译的文件的名字
__DATE__:表示编译时刻的日期字符串
__TIME__:表示编译时刻的时间字符串
__FUNCTION__:表示编译时候所在的函数名字
printf("代码在 %d 行\n", __LINE__);
printf("代码编译的时间%s %s\n", __DATE__, __TIME__);
printf("文件名 %s\n", __FILE__);
printf("函数名%s\n", __FUNCTION__);
程序段1
#else
程序段2
#endif
它的功能是:如果标识符已被#define命令定义过,则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(它为空),本格式种的#else可以没有,既可以写为:
#ifdef 标识符
程序段
#endif
#include <stdio.h>
int main()
{
#ifdef _DEBUG
printf ("正在使用DEBUG模式编译代码。。。\n");
#else
printf ("正在使用Release模式编译代码。。。。\n");
#endif
return 0;
}
编译的时候增加宏:gcc -D_DEBUG
程序段1
#else
程序段2
#endif
与上一种形式的区别是ifdef改为ifndef。它的功能是:如果标识符未被#define命令定义过,则对程序段1进行编译;否则对程序段2进行编译。这与第1种形式的功能正好相反
程序段1
#else
程序段2
#endif
它的功能是:如果常量表达式的值为真(非0),则对程序段1进行编译;否则对程序段2进行编译。因此可以使程序在不同条件下编译,完成不同的功能。
#include <stdio.h>
// window _WIN32
// Linux __linux__
int main()
{
#if (_WIN32)
{
printf("this is window system\n");
}
#elif (__linux__)
{
printf ("this is linux system\n");
}
#else
{
printf ("unkown system\n");
}
#endif
#if 0
code
#endif
return 0;
}
##用于在预编译期间粘连两个符号
#include <stdio.h>
#define SQR(x) printf ("The square of "#x" is %d \n", ((x)*(x)))
int main()
{
SQR(8);
return 0;
}
#include <stdio.h>
#define CREATEFUN(name1, name2)\
void name1##name2()\
{\
printf ("%s called\n", __FUNCTION__);\
}
CREATEFUN(my, Function);
int main()
{
myFunction();
return 0;
}
关键字
在局部静态变量前面加上关键字static,该局部变量便成了静态局部变量。静态局部变量有以下特点:
(1)该变量在全局数据区分配内存
(2)如果不显示初始化,那么将被隐式初始化为0
(3)它始终驻留在全局数据区,直到程序运行结束
(4)其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。
在全局变量前面加上关键字static,该全局变量变成了全局静态变量。
全局静态变量有以下特点:
(1)在全局数据区内分配内存
(2)如果没有初始化,其默认值为0
(3)该变量在本文件内从定义开始到文件结束可见,即只能在本文件内使用
在函数的返回类型加上static关键字,函数即被定义成静态函数。
静态函数有以下特点:
(1) 静态函数只能在本源文件中使用
(2) 在文件作用域中声明的inline函数默认为static
说明:静态函数只是一个普通的全局函数,只不过受static限制,他只能在所在文件内使用,不能在其他文件内使用。
int const i = 2; 或 const int i = 2;
定义或说明一个只读数组可采用如下格式:
int const a[5] = {1,2,3,4,5}; 或 const int a[5] = {1,2,3,4,5}
const int * p; // p可变,p指向的对象不可变
int const * p; // p可变,p指向的对象不可变
int * const p; // p不可变,p指向的对象可变
const int * const p; // 指针p和p指向的对象都不可变
这里给出一个记忆和理解的方法:
先忽略类型名(编译器解析的时候也是忽略类型名),我们看const离哪个近,“近水楼台先得月”,离谁近就修饰谁。
const (int) *p //const 修饰*p,p是指针,*p是指针指向的对象,不可变。
(int) const * p; //const 修饰*p,p是指针,*p是指针指向的对象,不可变。
( int) * const p;//const 修饰p,p不可变,p指向的对象可变
const ( int)* const p; // 前一个const修饰*p,后一个const修饰p,指针p和p指向的对象都不可变
const修饰也可以修饰函数的参数,当不希望这个参数值在函数体内被意外改变时使用,例如:
void Fun(const int *p);
告诉编译器*p在函数体中不能改变,从而防止了使用者的一些无意的或错误的修改。
const修饰符也可以修饰函数的返回值,返回值不可被改变。例如:
const int Fun(void);
使用关键字 typedef 可以为类型起一个新的别名,语法格式为:
typedef oldName newName;
oldName 是类型原来的名字,newName 是类型新的名字。
需要强调的是,typedef 是赋予现有类型一个新的名字,而不是创建新的类型。为了“见名知意”,请尽量使用含义明确的标识符,并且尽量大写。
typedef int INTEGER;
INTEGER a, b;
a = 1;
b = 2;
INTEGER a, b;等效于int a, b;
typedef char ARRAY20[20];
表示 ARRAY20 是类型char [20]的别名。它是一个长度为 20 的数组类型。接着可以用 ARRAY20 定义数组:
ARRAY20 a1, a2, s1, s2;
它等价于:
char a1[20], a2[20], s1[20], s2[20];
typedef struct stu{
char name[20];
int age;
char sex;
} STU;
STU 是 struct stu 的别名,可以用 STU 定义结构体变量:
STU body1,body2;
它等价于:
struct stu body1, body2;
typedef int (*PTR_TO_ARR)[4];
表示 PTR_TO_ARR 是类型int * [4]的别名,它是一个二维数组指针类型。接着可以使用 PTR_TO_ARR 定义二维数组指针:
PTR_TO_ARR p1, p2;
按照类似的写法,还可以为函数指针类型定义别名:
typedef int (*PTR_TO_FUNC)(int, int);
PTR_TO_FUNC pfunc;
1) 可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。如下所示:
#define INTERGE int
unsigned INTERGE n; //没问题
typedef int INTERGE;
unsigned INTERGE n; //错误,不能在 INTERGE 前面添加 unsigned
2) 在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。例如:
#define PTR_INT int *
PTR_INT p1, p2;
经过宏替换以后,第二行变为:
int *p1, p2;
这使得 p1、p2 成为不同的类型:p1 是指向 int 类型的指针,p2 是 int 类型。
相反,在下面的代码中:
typedef int * PTR_INT
PTR_INT p1, p2;
p1、p2 类型相同,它们都是指向 int 类型的指针。
gdb调试
要调试一个程序 首先要给程序在编译的时候加调试信息:
gcc XXX.c -g (编译的时候加-g)
启动调试:
gdb 可执行的程序
例如:
gdb a.out
n、f、u是可选的参数
n是一个正整数,表示需要显示的内存单元的个数,也就是说从当前地址向后显示几个内存单元的内容,一个内存单元的大小由后面的u定义。
f 表示显示的格式,参见下面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i。
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。
u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字 节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。
<addr>表示一个内存地址。
注意:严格区分n和u的关系,n表示单元个数,u表示每个单元的大小。