1.C语言中字符串的本质:
指针指向头、固定尾部的地址相连的一段内存。C语言中字符串有3个核心要点:第一是用一个指针指向字符串头;第二是固定尾部(字符串总是以'\0'来结尾);第三是组成字符串的各字符彼此地址相连。
2.char *p = "linux";
在这段代码中,p本质上是一个字符指针,占4字节;"linux"分配在只读常量区,占6个字节;实际上总共耗费了10个字节。sizeof测多少字节。strlen测字符串长度(不算'\0'),注意:只能传递合法的字符串才有意义
3.字符数组与字符串的本质差异(内存分配角度)
(1)字符数组char a[] = "linux";来说,定义了一个数组a,数组a占6字节,右值"linux"本身只存在于编译器中,编译器将它用来初始化字符数组a后丢弃掉(也就是说内存中是没有"linux"这个字符串的);这句就相当于是:char a[] = {'l', 'i', 'n', 'u', 'x', '\0'};
(2)字符串char *p = "linux";定义了一个字符指针p,p占4字节;同时还定义了一个字符串"linux",分配只读常量区,然后把常量区中的字符串(一共占6字节)的首地址(也就是'l'的地址)赋值给p。
总结对比:字符数组和字符串有本质差别。字符数组本身是数组,数组自身自带内存空间,可以用来存东西(所以数组类似于容器);而字符串本身是指针,本身永远只占4字节,而且这4个字节还不能用来存有效数据,所以只能把有效数据存到别的地方,然后把地址存在p中。也就是说字符数组自己存那些字符;字符串一定需要额外的内存来存那些字符,字符串本身只存真正的那些字符所在的内存空间的首地址。
4.结构体为何要对齐访问
结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐排布和访问会提高效率
对结构体取地址,是一字节类型的地址。错误理解!! &地址+1:加的是一个地址类型
5.内存对齐
关于结构体内存:(假设是4字节对齐)
1.想象一个以4字节为宽度的的矩形
2.一个 填充 的概念
3.注意每一个元素都有自己的对齐规则(判断填充,所以要看后面的类型,后面是前面的整数倍),如short(2字节),只能放在类似0,2,4这样的偶地址处
4.整个结构体是最宽元素的整数倍
例子:
struct mystruct //一共8字节
{
int a; //4
char b; //1+1
short c; //2
}
内存分配如图
gcc支持但不推荐的对齐指令:#pragma pack() #pragma pack(n) (n=1/2/4/8)
#pragma是用来指挥编译器,常用对齐命令:1. #pragma pack(),这种就是设置编译器1字节对齐 2. #pragma pack(4),这个括号中的数字就表示我们希望多少字节对齐。
gcc推荐的对齐指令__attribute__((packed)) __attribute__((aligned(n)))
__attribute__((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。
__attribute__((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐)
例子:
struct mystruct
{
int a;
char b;
short c;
} __attribute__((packed)); //取消对齐访问
// 那就是 4+1+2 =7字节
typedef struct mystruct111
{
int a;
char b;
short c;
short d;
}__attribute__((aligned(1024))) My111;
// 4 + 1 + 1(填充) + 2 + 2 =10字节,但是要填充1014字节
offsetof宏的原理
#define offsetof(TYPE,MEMBER) ( (int) &((TPYE *)0)->MEMBER ) //返回的是member元素相对整个结构体变量的首地址的偏移量,int类型
例:int offsetof_a= offsetof(struct stu, a)
(TYPE *)0 这是一个强制类型转换,把0地址强制类型转换成一个指针,这个指针指向一个TYPE类型的结构体变量。 (实际上这个结构体变量可能不存在,但是只要我不去解引用这个指针就不会出错)。
((TYPE *)0)->MEMBER (TYPE *)0是一个TYPE类型结构体变量的指针,通过指针指针来访问这个结构体变量的member元素
&((TYPE *)0)->MEMBER 等效于&( ((TYPE *)0)->MEMBER ),意义就是得到member元素的地址。
container_of宏
#define container_of(ptr, type, member ) ( { \
const tpyeof(((type *) 0) ->member) * __mptr = (prt); \
( type * ) ((char * ) __mptr - offsetof(type , member )) ; } ) //ptr 是指向结构体元素member的指针,作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针, 宏返回的是整个结构体变量的指针(由此可以得到其他结构体的指针)
例子:
char *ptr = &str.ch;
struct struct *p = container_of(ptr, struct srt, ch);
typeof(a) 由变量a得到a的类型;
const typeof( ((type *) 0) ->member ) * __mptr = (prt); //得到元素类型的指针:例const char* __mptr = prt ;
( type * ) ((char * ) __mptr - offsetof(type , member )) ; //先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的)(注意转成char *),减去之后得到的就是整个结构体变量的首地址了,再把这个地址强制类型转换为type *即可! 没问题
共用体union
共用体union就是对同一块内存中存储的二进制的不同的理解方式。
union的sizeof测到的大小是union中各个元素里面占用内存最大的那个元素所占字节的倍数
大端模式(big endian)和小端模式(little endian)
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。ARM是小端模式。
2种测试方法:
//通过指针
int test_endian1(void)
{
int i = 1;
char c = *((char *)(&i));
return (1 == c);
}
//通过union
union myUnion
{
int i;
char c;
};
int test_endian2(void)
{
union myUnion u1;
u1.i = 1;
return (u1.c == 1);
}
思考几种看似可行,其实不行的方式: 原因都是这个运算是高于内存层次的
// 位与
int a = 1;
int b = a & 0xff;
printf("b = %d.\n", b);
理论分析:位与运算是编译器提供的运算,这个运算是高于内存层次的(或者说&运算在二进制层次具有可移植性,也就是说&的时候一定是高字节&高字节,低字节&低字节,和二进制存储无关)。
// 移位
int a, b;
a = 1;
b = a >> 1;
printf("b = %d.\n", b);
理论分析:原因和&运算符不能测试一样,因为C语言对运算符的级别是高于二进制层次的。右移运算永远是将低字节移除,而和二进制存储时这个低字节在高位还是低位无关的。
// 强制类型转换
int a;
char b;
a = 1;
b = (char)a;
printf("b = %d.\n", b);
分析:同上
枚举
默认从0开始,初始直接赋值。
宏定义与枚举区别:
(1)枚举是将多个有关联的符号封装在一个枚举中,而宏定义是完全散的。也就是说枚举其实是多选一。
(2)什么情况下用枚举?当我们要定义的常量是一个有限集合时(譬如一星期有7天,譬如一个月有31天,譬如一年有12个月····),最适合用枚举。(其实宏定义也行,但是枚举更好)
(3)不能用枚举的情况下(定义的常量符号之间无关联,或者无限的)用宏定义。
总结:宏定义先出现,用来解决符号常量的问题;后来人们发现有时候定义的符号常量彼此之间有关联(多选一的关系),用宏定义来做虽然可以但是不贴切,于是乎发明了枚举来解决这种情况。
enumeration 类型定义
// 定义方法1,定义类型和定义变量分离开 enum week { SUN, // SUN = 0 MON, // MON = 1; TUE, WEN, THU, FRI, SAT, }; enum week today; */
// 定义方法2,定义类型的同时定义变量 enum week { SUN, // SUN = 0 MON, // MON = 1; TUE, WEN, THU, FRI, SAT, }today,yesterday; */
// 定义方法3,用typedef定义枚举类型别名,并在后面使用别名进行变量定义 typedef enum week { SUN, // SUN = 0 MON, // MON = 1; TUE, WEN, THU, FRI, SAT, }week; */
错误类型举例
// 错误1,枚举成员重名,编译时报错:redeclaration of enumerator ‘MON’ typedef enum workday { MON, TUE, WEN, THU, FRI, }workday; typedef enum weekend { MON, SAT, SUN, }weekend;
// 结构体中元素可以重名 typedef struct { int a; char b; }st1; typedef struct { int a; char b; }st2;
解释: 不能有重名的枚举成员。 分析:经过测试,两个struct类型内的成员名称可以重名,而两个enum类型中的成员不可以重名。实际上从两者的成员 在访问方式上的不同就可以看出了。struct类型成员的访问方式是:变量名.成员,而enum成员的访问方式为:成员名。 因此若两个enum类型中有重名的成员,那代码中访问这个成员时到底指的是哪个enum中的成员呢? 两个#define宏定义是可以重名的,该宏名真正的值取决于最后一次定义的值。编译器会给出警告但不会error