说在前面
用了2-3周时间做了小组往年的纳新题之后,经过了同级小伙伴的共同讲解以及学长的拓展补充,感觉对c有了更深的认识,所以在这记录一下(恩…同时也是应小组任务)
一个c程序
开始先让我们来探索一下一个c程序如何从源代码到形成可执行文件
A:预处理 —— 宏定义/条件编译/文件包含
B:编译 —— 检查语法,生成汇编
C:汇编—— 汇编代码转换成机械码
D:链接 —— 链接到一起生成可执行文件
注:预处理过程还会删除注释以及多余的空白符
你不知道的main函数
main???
int main(int argc,char *argv[])
{
return 0;
}
C语言标准中强制要求main函数的返回值类型为int,main函数的返回值是传递给操作系统,让操作系统判断程序的执行情况
argc:表示命令行中参数的个数(文件名本身也是),argc的值是在输入命令时由系统按实际参数的个数自动赋予的
argv:字符串指针数组,其各元素值为命令行中各字符串的首地址,数组长度为参数个数(argc),各元素初值由系统自动赋予
printf 的世界
再了解了一个c语言程序的形成过程后,让我们来进入printf的世界
int main(int argc, char *argv[])
{
int a = 10, b = 20, c = 30;
printf("%d %d\n", b = b*c, c = c*2) ;
printf("%d\n", printf("%d ", a+b+c));
return 0;
}
这里就不饶关子了,输出是:
1200 60
1270 5
看到结果可能有些人就有疑惑了,为什么会是这样呢?这就关系到printf的运行机制了
||||||
printf运行机制为在栈上申请储存空间,其规则为先进后出,因此printf表现为参数从右向左运算,而printf的返回值为其打印字符的列宽
相信理解了printf的内在后,你会一眼视传上面的题目的
机器的大小端
1.大小端?
在计算机系统中以字节为单位,每个地址单位都有一个字节(一个字节8个bit位)。在C语言中,有char(8bit)、short(16bit)、int(32bit,具体看编译器)。对于位数大于8的处理器,如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在一个如何将多个字节安排的问题。这就有了大端存储模式和小端存储模式
2.具体概念
a.大端:数据的高字节保存在内存的低地址处,数据的低字节保存在内存的高地址
b.小端:数据的高字节保存在内存的高地址处,数据的低字节保存在内存的低地址
例如:
int a = 0x12345678
内存地址 大端 小端
0x0000 0x12 0x78
0x0001 0x34 0x56
0x0002 0x56 0x34
0x0004 0x78 0x12
注:具体判断方法可以使用联合体或者指针
c中的static
static int a = 2018; //静态内部链接的文件作用域
void func(void)
{
static int b; //静态无链接的块作用域
printf("a = %d, b = %d\n",a++,++b);
}
int main(int argc,char *argv[])
{
func();
func();
func();
return 0;
}
输出:
2018 1
2019 2
2020 3
静态局部变量使用static修饰符定义,即使在声明时未赋初值,编译器也会把它初始化为0
静态局部变量存储于进程的全局数据区,即使函数返回,它的值也会保持不变
静态全局变量仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响
static函数的使用方式与全局变量类似
注:b定义在函数块内,只有执行函数时才会被调用
sizeof 与 strlen
void func(char *a)
{
printf("%lu\n", sizeof(a));
printf("%lu\n", strlen(a));
}
int main(int argc, char *argv[])
{
char a[] = "hello world";
func(a);
printf("%lu\n", sizeof(a));
printf("%lu\n", strlen(a));
return 0;
}
输出:8 11 12 11
为什么第一个sizeof是8,而第二个是12呢?
第一个sizeof测的实际上是一个指针,指针在64位机下为8个字节,而第二个sizeof测的才是a这个数组的大小
sizeof在编译时计算缓冲区的长度,返回size_t类型(无符号整型),以字节为单位返回运算对象的大小(包括null字符)
strlen用于统计字符串的长度(不包括null字符),这样就能解释为什么sizeof是12,而strlen是11了
无符号整型与有符号整型运算
void func(void)
{
unsigned int a = 6 ;
int b = -20 ;
(a+b>6) ? puts(“>6”) : puts(“<6”);
}
//应该输出什么呢?
//答案:>6
//why?
6 + (-20) ?= -14
这就关系到无符号整型与有符号整型运算规律了,首先我们需要把int——>unsigned int ,而在计算机中是以二进制运算的,负数需要用补码计算(你可以试着转换一下,会发现这是一个非常大的数)
注:不想转换为二进制的话,可以 -20 + 2^32 ,这样就转换为unsigned int 了(还挺好记简单的)
结构体内存对齐
struct icd
{
int a;
char b;
double c;
};
struct cdi
{
char a;
double b;
int c;
};
int main(int argc,char *argv[])
{
printf("%zu %zu\n",sizeof(struct icd),sizeof(struct cdi));
return 0;
}
输出:16 24
只是调换了下位置,为什么结果都不同了?
这就让我们来了解下内存对齐规则吧
1.数据成员对齐:第一个成员从0的地址开始,以后每一个成员储存的起始位置为该成员大小的整数倍
2.结构体作为成员:如果结构体1作为另一个结构体2的成员,则在2中1要从1内部成员最大的整数倍地址开始储存
3.总大小(sizeof):为该结构体内部最大基本类型的整数倍,不足要补齐
const关键字
char y[ ] = "XiyouLinuxGroup", x[ ] = "2018";
char *const p1 = y;
const char *p2 = y;
/* (1) */ p1 = x; //错
/* (2) */ p2 = x; //对
/* (3) */ *p1 = 'x'; //对
/* (4) */ *p2 = 'x'; //错
//why?
const在 * 的右边,代表指针不可改变,但是却可以使用指针使所指向的对象的值重置(顶层const)
const在 * 的左边,代表指针所指向的对象不可改变,但是却可以使指针指向别的对象,只是不可改变所指向对象的值(底层const)
注:一个对象可以同时具有双层const
在#define中使用参数
#define NAME(n) x##n
#define PRINTVAL(y, ...) printf("x"#y":" __VA_ARGS__)
int main(int argc, char *argv[])
{
int NAME(1);
short *NAME(2) = ( short *)&NAME(1);
char *NAME(3) = (char *)&NAME(1);
NAME(1) = 0;
*NAME(2) = -1;
PRINTVAL(1, "%x\n", NAME(1));
PRINTVAL(2, "%x\n", *NAME(2));
PRINTVAL(3, "%x\n", *NAME(3));
return 0;
}
//相信很多人看到这题会有点懵...
用宏参数创建字符串:#运算符——#号作为一个预处理运算符,可以把记号转换为字符串
预处理器粘合器:##运算符——可用于类函数宏/对象宏的替换部分,可以把两个记号合成一个记号
变参宏:…和__VA_ARGS__:__VA_ARGS__可用在替换部分中,表示省略号代表什么
上题的输出为:
x1:ffff
x2:ffffffff
x3:ffffffff
看到答案可能会更懵…
先定义一个int型变量x1,然后强制让short,char指针指向x1
然后x1 = 0,x2 = -1,因为x2指向x1,所以x1 = 0在这里就没什么意义了
最后使用%x输出,但是明明是一块地址,为什么值不一样?
因为int比short多2字节,所以 * x2 = -1时(由于在计算机内部是二进制运算)short转为int是会把short2个字节的内容移到int的2个低字节上,然后高位补全(补0),而char比short少1字节,所以会低位截取
最后由于%x默认输出unsigned int ,所以 * x2, * x3 都为ffffffff
最后说一下数组吧
int main(int argc, char *argv[])
{
int a[3][4];
printf("%p%p%p%p%p%p\n", &a[0][0], a[0],a, a[0]+1, a+1, a[1]);
return 0;
}
*1. sizeof(a),a的类型:int [3][4],所以它返回整个数组的长度
2. fun(int a),作为函数形参,a的类型为int *
3. &a,它的类型为int ( * )[3][4],是一个数组指针
4. 在表达式中,它的类型为int (const * )[4],即指向数组首元素a[0]的常量指针,a[0]它是一个int[4]
5. c语言中严格来说没有什么二维数组,它只有那种普通的一维数组,二维数组可以看做数组的数组,即对于a来说,它有3个元素,每个元素都是一个具有4个整形元素的一维数组(int[4])。我们可以把a[0]看做第一个元素的数组名,其他类推。
6. 显然 * a = * (a + 0) = * (0 + a) = a[0] = 0[a],加法具有交换律嘛。那么,a = &a[0],这时就验证了我们上面的说法,此时(在表达式中),a是一个int( * )[4]
7. a + 1 <==> &a[0] + 1,就是跨过第一行,即一个int[4]。a[0] + 1就是跨过第一行的第一个元素,即&a[0][1]。&a + 1自然就跨过整个数组喽
到这本次总结就结束了,在学习的路上还是任重而道远,加油!!!
!@#$%^&*~