(1)
blog.csdn.net/qq_39412582…
(2)
baike.baidu.com/item/%E5%A4…
(3)
blog.csdn.net/qq_29350001…
大端存储
:就是把一个数的低位字节序的内容存放到高地址处,高位字节序的内容存放在低地址处。
小端存储
:就是把一个数的低位字节序的内容存放到低地址处,高位字节序的内容存放在高地址处。
从定义可以看出,大端存储与我们人眼的习惯相符合。
速记:低字节序在哪就是哪。
1.1 怎么检测设备是是按大端存储还是小端存储
#include <stdio.h>
int main()
short int x;
char x0;
x=0x1122;
x0=((char*)&x)[0];
if (x0 == 0x11) {
printf("big endian");
} else if (x0 == 0x22) {
printf("little endian");
return 0;
作者在window上跑这段程序是小端存储。由于没有大端设备,所以这里没法验证大端的情况。(我找了在线运行的网站,都是小端设备。如果有哪位小伙伴知道大端在线运行的网站,请一定给我留言。)
但是这里其实要和截断区别,截断都是截的低位字节的,与是低地址还是高地址无关。
比如,下面这段程序,由于低位字节是0x78,所以它的运行结果不管是在大端设备还是小端设备,都应该是输出0x78。(没有设备验证)
#include <stdio.h>
int main()
char x1;
int x2 = 0x12345678;
x1 = x2;
printf("\n%x", x1);
return 0;
1.2 如何进行大小端的转换
1.2.1 方法一:位操作
#include<stdio.h>
typedef unsigned int uint_32 ;
typedef unsigned short uint_16 ;
#define BSWAP_16(x) \
(uint_16)((((uint_16)(x) & 0x00ff) << 8) | \
(((uint_16)(x) & 0xff00) >> 8) \
#define BSWAP_32(x) \
(uint_32)((((uint_32)(x) & 0xff000000) >> 24) | \
(((uint_32)(x) & 0x00ff0000) >> 8) | \
(((uint_32)(x) & 0x0000ff00) << 8) | \
(((uint_32)(x) & 0x000000ff) << 24) \
int main()
printf("%#x\n",BSWAP_16(0x1234));
printf("%#x\n",BSWAP_32(0x12345678));
return 0 ;
1.2.2 方法二:使用 htonl, htons, ntohl, ntohs 等函数
htonl()
htons()
ntohl()
ntohs()
注,主机字节顺序,X86一般多为小端(little-endian),网络字节顺序,即大端(big-endian);
#include <stdio.h>
#include <arpa/inet.h>
int main ()
union
short i;
char a[2];
u.a[0] = 0x11;
u.a[1] = 0x22;
printf ("0x%x\n", u.i);
printf ("0x%.x\n", htons (u.i));
return 0;
0x2211
0x1122
2 内联(inline)
以 inline 修饰的函数叫内联函数。内联函数在调用时不是像一般函数那样要转去执行被调用的函数体,执行完后再转回调用函数中,执行之后的语句;而是在调用处用内联函数体的代码来替换,这样没有函数压栈,将会节省调用的开销,提高运行效率。
内联函数必须是和函数体定义在一起才有效。
内联函数和宏的区别:
1. 内联函数在运行时可调试,而宏定义不可以
2. 编译时会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义不会
何时使用内联函数: 建议当函数在 10 行以内时才将其定义为内联函数。
以下情况不宜使用内联:
(1) 如果函数体内的代码比较长、使用内联将导致内存消耗代价较高
(2) 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
3 数据类型转换
参考博客: blog.csdn.net/zhangzhi123…
一个原则:
若要扩展量为有符号量,不管扩展成有符号还是无符号,都遵循符号扩展;若要扩展量为无符号量,不管扩展成有符号还是无符号,都遵循零扩展。
char a = 0xff; // a为-1,其为有符号量,二进制为11111111
unsined short b = a; // 此处a要进行符号扩展, b的二进制为1111111111111111
unsigned char a = 0xff; // a为无符号量,二进制为11111111
short b = a; // 此处a要进行零扩展,b的二进制为00000000 11111111
(1) 短数据类型扩展为长数据类型: 要扩展的短数据类型为有符号数的,进行符号扩展。要扩展的短数据类型为无符号数,进行零扩展。
(2) 长数据类型缩减为短数据类型: 如果长数据类型的高字节全为1或全为0,则会直接截取低字节赋给短数据类型;如果长数据类型的高字节不全为1或不全为0,则转会就会发生错误。
(3) 同一长度的数据类型中有符号数与无符号数的相互转化: 直接将内存中的数据赋给要转化的类型,数值大小则会发生变化。另短类型扩展为长类型时,但短类型与长类型分属有符号数与无符号数时,则先按规则一进行类型的扩展,再按本规则直接将内存中的数值原封不动的赋给对方。
4. 字节对齐
struct/union 空间大小计算原则:
结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
结构体每个成员相对于结构体首地址的偏移量都是成员自身大小的整数倍,如有需要编译器会在成员之间加上填充字节;
结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
字节对齐的定义:
自然对齐(数据类型的长度)
(1)对于 char 型数据,自然对齐的长度为 1 字节,short 为 2 字节,int 为 4 字节,float 为 4 字节,当然这些数据类型的对齐长度可能和编译器有关。
(2)结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
指定对齐:
#progma pack(n)中 n 的值,如果没有指定,则为 CPU 默认对齐值,32 位默认 4 字节对齐,64 位默认 8 字节对齐。
有效对齐:
自然对齐和指定对齐的最小值
结构体对齐步骤:
(1) 每个成员变量分别按自己的"有效对齐"作对齐
(2) 取所有成员变量的"有效对齐"的最大值,作为结构体的"有效对齐"
(3) 结构体成员及结构体自己的起始地址和长度均为其“有效对齐”的整数倍
#include <stdio.h>
struct tagA {
char a;
short b;
char d;
struct tagB
char a;
int b;
char d;
struct tagC
char a;
short b;
int c;
char d;
int main()
struct tagA a, b[2];
printf("struct tagA :sizeof(a):%d, sizeof(b):%d\n", sizeof(a), sizeof(b));
struct tagB c, d[2];
printf("struct tagB :sizeof(c):%d, sizeof(d):%d\n", sizeof(c), sizeof(d));
struct tagC e, f[2];
printf("struct tagC :sizeof(e):%d, sizeof(f):%d\n", sizeof(e), sizeof(f));
return 0;
按照结构体对齐步骤分析:首先分析结构体 tagA,它的成员的有效对齐是 a 是 1,b 是 2,d 是 1。所以结构体 tagA 的有效对齐是 2。最后,tagA 的成员 a 的起始地址是 1 的整数倍即可,b 的起始地址是 2 的整数倍即可,d 的起始地址是 1 的整数倍。结构体 tagA 的总长度是 2 的整数倍。所以结构体 tagA 的长度是 6.所以 stb[2]的长度是 12.
其他的类似分析。
运行结果:
struct tagA :sizeof(a):6, sizeof(b):12
struct tagB :sizeof(c):12, sizeof(d):24
struct tagC :sizeof(e):12, sizeof(f):24
5 预处理器
5.1 宏
#define name stuff
每当有符号 name 出现在这条指令后面时,预处理器就会把它替换成 stuff。注意,后面不要带分号。如果定义中的 stuff 非常长,它可以分成几行,除了最后一行外,每行的末尾都要加一个反斜杠,例如:
"x=%d, y=%d, z=%d", \
__FILE__, __LINE__, \
x, y, z)
带参数的宏
#define name(parameter-list) stuff
#define SQUARE(x) x * x
所以,SQUARE(5),预处理器就会替换成 5 * 5
但是如果:
a = 5
printf("%d\n", SQUARE(a + 1))
本意以为会输出 36。但是实际上我们将用到的宏的地方替换,其实是
printf("%d\n", a+1 * a + 1);
即输出的是 11。
为了解决这个问题,只要在宏参数中加上两个括号就行:
#define SQUARE(x) (x) * (x)
但是现在有另一个宏
#define DOUBLE(x) (x) + (x)
又会出现另一个问题。例如:
a = 5
printf("%d\n", 10 * DOUBLE(a))
我们期望的值是 100。但是通过宏展开得到:
printf("%d\n", 10 *(x) + (x));
得到的结果是 55。
这个错误也很容易修正:只要在整个表达式两边加上一对括号即可:
#define DOUBLE(x) ((x) + (x))
所以,一般在定义宏时,首先每个宏参数都加上括号,其次整体表达式也要加上一对括号。
我们经常用宏来执行一些简单地计算,比如选出两个数中较大的一个:
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
为啥不用函数呢?
1、用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,所以使用宏比使用函数在程序的规模和速度方面都更胜一筹。
2、更重要的是,函数的参数必须声明为一种特殊的类型,所以它只能在类型合适的表达式上使用。但是宏是与类型无关的。例如,上面这个宏可以用于整型、长整型、单浮点型、双浮点型等。
但是和函数比,宏的缺点是每次使用宏时,一份宏定义代码的拷贝都将插入到程序中。除非宏非常短,否则使用宏可能会大幅增加程序的长度。一般建议宏定义不要超过 10 行。另外,有些任务是函数完成不了的。
#define MALLOC(n, type) \
( (type *)malloc((n) * sizeof(type)))
pi = MALLOC(25, int);
将被替换成:
pi = ( (int *)malloc((25) * sizeof(int)));
不要使用带副作用的宏参数
当宏参数在宏定义中出现的次数超过一次时,如果这个参数有副作用,那么当你使用这个宏时就可能出现危险,导致不可预料的结果。
x = 5
y = 8
z = MAX(x++, y++)
printf("x=%d, y=%d, z= %d\n", x, y, z);
第一个表达式是条件表达式,用于确定执行两个表达式中的哪一个,剩余的那个表达式将不会执行。
那上面这段代码的输出是多少呢?
x =6, y=10, z= 9;
为什么呢?
我们将宏定义展开:
z = ( (x++) > ( y++) ? (x++) : (y++))
首先是比较两个表达式,比较完后,x= 6, y = 9.并且由于 y 比 x 大,所以在比较完后 y 还会再执行一次 y++。所以最终的结果是 y =10。
#undef
#undef name
这条预处理指令由于移除一个宏定义。
如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用。#undef 移除。
预处理器运算符
(1) 字符串常量化运算符(#):在宏定义中,当需要把一个宏的参数转换成字符串常量时,可以使用字符串常量运算符(#):
#include <stdio.h>
#define P(A) printf("%s:%d\n", #A, A)
int main() {
int a = 1;
P(a);
return 0;
a:1
(2)标记粘贴运算符(##):宏定义内的标记粘贴运算符(##)会合并两个参数。
#include <stdio.h>
#define P(A, B) printf("%d##%d = %d", A, B, A##B)
int main() {
P(5, 6);
return 0;
5#
注意:当宏参数是另一个宏的时候,需要注意的是凡宏定义中有用#或者##的地方宏参数时不会再展开:
#include <stdio.h>
#define f(x, y) x##y
#define g(x) #x
#define h(x) g(x)
int main()
printf("%s, %s\n", g(f(1, 2)), h(f(1,2)));
return 0;
解析:第一个表达式 g(f(1,2)),g(x)的定义中有#,不展开 f(x,y)的宏,直接替换成#f(1,2),打印输出为 f(1,2)。
第二个表达式 h(f(1, 2)),h(x)的定义中没有#或者##,需要展开 f(x, y)的宏, 即 1##2,即 h(12)->g(12), 最终结果是 12。
宏 | 含义 |
---|
FILE | 进行编译的源文件名 |
LINE | 文件当前行的行号 |
DATE | 文件被编译的日期 |
TIME | 文件被编译的时间 |
STDC | 如果编译器遵循 ANSI C,其值为 1,否则未定义 |
预处理器和宏定义——typedef 和 define
typedef 与 define 都是替一个对象取一个别名,以此来增强程序的可读性,但是它们在使用和作用上也存在以下几个不同:
(1) 原理不同。#define 是 C 语言中定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,不作正确性检查,不管含义是否正确照样带入,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,它在编译时出来,所以 typedef 有类型检查的功能。
(2) 功能不同。typedef 是用来定义类型的别名,这些类型不只包含内部类型(int 、char 等),还包括自定义类型(如 struct),可以起到使类型易于记忆的功能。例如定义一个函数指针。#define 不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
(3) 作用域不同。#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。
预处理器与宏定义——define 和 const
define 与 const 都能定义常量,效果虽然一样,但是各有侧重。define 既可以替代常数值,又可以替代表达式,甚至是代码段,但是容易出错。而 const 的引入可以增强程序的可读性,它使程序的维护与调试变得更加方便。具体而言,它们的差异主要表现在以下几个方面:
(1) define 只是用来进行单纯的文本替换,define 常量的生命周期止于编译期,不分配内存空间,它存在于程序的代码段,在实际程序中它只是一个常数,一个命令中的参数并没有实际的存在;而 const 常量存在于程序的数据段,并在堆栈中分配了空间,const 常量在程序中确确实实的存在,并且可以被调用、传递。
(2) const 常量有数据类型,而 define 常量没有数据类型。编译器可以对 const 常量进行类型安全检查,如类型、语句结构等,而 define 不行。
5.2 条件编译
#if constant-expression
statements
#endif
#if constant-expression
statements
#elif constant-expression
other statements
#else
other statements
#endif
是否被定义:
#if defined(symbol)
#ifdef symbol
#if !defined(sysmbol)
#ifndef symbol
5.3 文件包含
#include 指令使另一个文件的内容被编译,就像它实际出现于#include指令出现的位置一样。这种替换执行的方式很简单:预处理器删除这条指令,并用包含文件的内容取而代之。所以,如果一个头文件被包含到 10 个源文件中了,它实际会被编译 10 次。但是并不会对运行时效率有影响。
#include <filename>
#include "filename"
嵌套文件包含
嵌套文件包含的一个不利之处在于一个头文件可能会被多次包含,比如:
#include "a.h"
#include "b.h"
如果 a.h 和 b.h 都包含一个嵌套的#include 文件 x.h,那么 x.h 在此处会出现两次。
为了解决这个问题,一般头文件这样写:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H 1
......
#endif
当头文件第一次被包含时,它被正常处理,符号_HEADERNAME 被定义成 1.如果头文件再次被包含,通过条件编译,它的所有内容被忽略。
上面定义也可以写成这样:
#define _HEADERNAME_H
请见另一篇文章C语言指针总结。
7 变量存储
BSS段(bss segment):通常是指用来存放程序中未初始化的全局变量(包括静态变量)和初始化为 0 的的全局变量(包括静态变量)的一块区域。BSS 是英文 Block Started By Symbol 的简称。BSS 属于静态内存分配。
Data 段(data segment):数据段,用来存放已经初始化且初始化值为非零的全局变量(包括静态变量),数据段属于静态内存分配。
代码段(code segment/text segment):通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。
字符串常量(rodata):该区域存放的是字符常量,属于只读区域,有些教材把这个区域归位于代码段。
堆(heap):堆是用来存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或收缩。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用 free 等函数释放内存时,被释放的内存从堆中被踢除(堆被缩减)
栈(stack):栈又称堆栈,是用户存放程序临时创建的局部变量(不包括静态局部变量。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中)
#include <stdio.h>
int bss_array[1024 * 1024] = {0};
int main()
return 0;
变量 bss_array 的大小为 4 *1024*1024 = 4M。但是其可执行文件的大小却远没有 4M。说明 bss 类型的全局变量只占运行时的内存空间,而不占文件空间。
但是把上面的数组赋值为非零时:
#include <stdio.h>
int data_array[1024 * 1024] = {1};
int main()
return 0;
文件大小就变为 4M 多了。由此可见,data 类型的全局变量是既占文件空间,又占运行时内存空间的。
8 static 关键字
对全局变量增加 static 修饰,将导致该变量的作用域发生变化,该变量只在该文件中生效。
(全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并没有不同)
对局部变量增加 static 修饰,将导致该变量的存储区域发生改变,该变量从栈变为静态区域(可能是 bss 段,也可能是 data 段)
初始化:static 局部变量只被初始化一次,下一次依据上一次的结果值。
存储方式:普通局部变量存放在栈中,而 static 局部变量存放在静态区。
对于如下程序:
int c
void func(int a)
static int b
int array[5] = {0}
int *p = (int *)malloc(100)
b = a
c = b + 1
return
以下说法正确的是:
A:放在栈中的变量有 a, p, array;
B:放在栈中的变量有 a, array, b;
C:放在 BSS 段里的变量有 b,c
D:放在堆中的是 p 指向的内存
答案:ACD
9 Cache 存储器与二维数组
cpu 的缓存机制:cpu 从内存中读取数据的时候,会先把读取的数据加载到 cpu 的缓存中。而 cpu 每次从内存中读取数据并不是只读取那个特定要访问的地址,而是读取一个数据块(一段连续的内存地址)并保存到 cpu 的缓存中,然后下次访问内存数据的时候就会先从 cpu 缓存开始查找,如果找到就不需要再从内存中取。这样就实现了比内存访问速度更快的机制,这同时也是 cpu 缓存存在的意义:为了弥补内存访问速度过慢与 cpu 执行速度快之间的差异而引入的。
由于数组是行优先的。所以访问同样一个二维数组。你先访问行还是先访问列,其实性能上是有区别的。
看下面的一段代码:
int a[MAX_SIZE][MAX_SIZE]
void func1()
clock_t startTime
clock_t endTime
startTime = clock()
for (int i = 0
for (int j = 0
a[i][j] = i + j
endTime = clock()
double total_time = (double) (endTime - startTime) / CLOCKS_PER_SEC
printf("func1: %f seconds\n", total_time)
return
void func2()
clock_t startTime
clock_t endTime
startTime = clock()
for (int j = 0
for (int i = 0
a[i][j] = i + j
endTime = clock()
double total_time = (double) (endTime - startTime) / CLOCKS_PER_SEC
printf("func2: %f seconds\n", total_time)
return
int main() {
func1()
func2()
return 0
func1 和 func2 都是对二维数组 a 赋值。只是它们访问的顺序不同:func1 先访问行,再访问列。func2 是先访问列,再访问行。
但是它们的运行时间却有区别:从下面的结果可以看到,func1 的执行时间比 func2 的少。
造成这个差异的原因就是上面所说的的 cpu 缓存机制。由于数组是行优先,所以对于 func1。当它访问元素 a[0][0]时,由于下面要访问的是它的地址 a[0][1]、a[0][2]......。这些地址会命中 cache。所以就直接从 cache 中读取了,而不必执行访问内存的操作了。但是对于 func2。当它读 a[0][0]时,接下来读取的是 a[1][0]、a[2][0]。这些地址都不在这个 cache 中,所以接下来每访问一个元素,它就必须执行一次访问内存的这个操作,进而 func2 的性能肯定会比 func1 的差。
10 C 语言常用的字符串函数总结
参考博客: www.cnblogs.com/LydiammZuo/…
strlen
原型:size_t strlen(char const* string);
功能:返回字符串 string 的长度(不包含字符串终止符 NUL)
注意:size_t 是一个无符号整数类型
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
char* y = "abcdef";
char* x = "abcd";
if (strlen(x) >= strlen(y)) {
printf("1: strlen(x) >= strlen(y)\n");
} else {
printf("1: strlen(x) < strlen(y)\n");