C语言基础-复合类型数据,编译相关,关键字,位运算,内存管理,gdb调试

来源:互联网 发布:淘宝美工和室内设计师 编辑:程序博客网 时间:2024/05/22 06:19

结构体




结构体内存对齐模式

数据项只能存储在地址是数据项大小的整数倍的内存位置上
 
 
例如int类型占用4个字节,地址只能在0,4,8等位置上。
每 个操作系统都有自己的默认内存对齐系数,如果是新版本的操作系统,默认对齐系数一般都是8,因为操作系统定义的最大类型存储单元就是8个字节,例如 long long,不存在超过8个字节的类型(例如int是4,char是1,long在32位编译时是4,64位编译时是 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的整数倍开始的,为了增加读取的效率)!

有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。例如开关只有通电和断电两种状态,用 0 和 1 表示足以,也就是用一个二进位。正是基于这种考虑,C语言又提供了一种叫做位域的数据结构。
 在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域:
 
struct bs{
    unsigned m;
    unsigned n: 4;
    unsigned char ch: 6;
};
当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
struct bs{
unsigned m: 6;
unsigned n: 12;
unsigned p: 4;
};
printf("%d\n", sizeof(struct bs));
当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC 会压缩存储,而 VC/VS 不会
struct bs
{
unsigned m: 12;
unsigned char ch: 4;
unsigned p: 4;
};
printf("%d\n", sizeof(struct bs));

如果成员之间穿插着非位域成员,会视情况进行压缩。例如对于下面的 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。
union 共用体名
{
   成员列表
};
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
共用体的所有成员起始地址的是一样的。
 


// 如果是小端模式,返回ture,否则返回false
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 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量
 
 





预编译相关

1、#define定义宏常量可以出现在代码的任何地方
2、从#define宏定义位置开始,以后的代码就都可以使用这个宏了
3、编译器会在预处理的时候用真身替换宏
数值宏常量
#define PI     3.1415926
#define ERROR  -1
字符串宏常量
#define STR_1 "hello"
#define STR_2  hello
#define STR_3  "hell\
o"
反斜杠作为接续符的时候,在本行后面不能再有任何字符,空格都不行。
 
#define SUM(a, b) ((a)+(b))
#define MIN(a, b) ((a)<(b) ? (a) : (b))

 
#define PI 3.1415926
 
#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__);
 

#ifdef 标识符
程序段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
 

#ifndef 标识符
程序段1
#else
程序段2
#endif
 
与上一种形式的区别是ifdef改为ifndef。它的功能是:如果标识符未被#define命令定义过,则对程序段1进行编译;否则对程序段2进行编译。这与第1种形式的功能正好相反
 
#if 常量表达式
程序段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;
}
 


关键字

extern可置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量或函数时,在其它模块中寻找其定义。



在局部静态变量前面加上关键字static,该局部变量便成了静态局部变量。静态局部变量有以下特点:
(1)该变量在全局数据区分配内存
(2)如果不显示初始化,那么将被隐式初始化为0
(3)它始终驻留在全局数据区,直到程序运行结束
(4)其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。
 在全局变量前面加上关键字static,该全局变量变成了全局静态变量。
全局静态变量有以下特点:
(1)在全局数据区内分配内存
(2)如果没有初始化,其默认值为0
(3)该变量在本文件内从定义开始到文件结束可见,即只能在本文件内使用
 在函数的返回类型加上static关键字,函数即被定义成静态函数。
静态函数有以下特点:
(1) 静态函数只能在本源文件中使用
(2) 在文件作用域中声明的inline函数默认为static
说明:静态函数只是一个普通的全局函数,只不过受static限制,他只能在所在文件内使用,不能在其他文件内使用。
 


一般变量是指简单类型的只读变量。这种只读变量在定义时,修饰符const可以用在类型说明符前,也可以用在类型说明符后。例如:
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;

typedef 在表现上有时候类似于 #define,但它和宏替换之间存在一个关键性的区别。正确思考这个问题的方法就是把 typedef 看成一种彻底的“封装”类型,声明之后不能再往里面增加别的东西。
 
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调试

gdb 调试:
要调试一个程序 首先要给程序在编译的时候加调试信息:
gcc XXX.c  -g  (编译的时候加-g)
 
启动调试:
gdb 可执行的程序
例如:
gdb  a.out


x/<n/f/u> <addr>
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表示每个单元的大小。



内存
 
内存管理中不同栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。
阅读全文
0 0
原创粉丝点击