1.内存分区
先来看看程序运行时的5个大的内存分区:
栈区 | 由编译器自动分配释放,存放函数参数值,局部变量的值,类似于数据结构中的栈 |
---|---|
堆区 | 由程序员分配释放,如果程序员不释放,程序结束时可能由操作系统回收,malloc或者new出来的在这里存放 |
代码区 | 顾名思义,存放程序代码的,只读区域 |
数据区 | 1.未初始化: BSS 段存放的是未初始化的全局变量与静态变量,并且都被赋初值为0 |
2.已初始化: (1)全局区 :可读可写,存放函数外部定义的全局变量与局部变量 ——————————————————————————— (2)常量区:常量区是只读的,不可修改的,程序结束后由系统释放 |
为什么bss段存放的变量都会被赋初值为0?
这是因为当可执行文件运行前,会为bss段中的变量分配足够的内存空间并自动清理
关于更多与bss段相关的信息可以看这个博主的,有些关于bss段的部分还没有完全理解
关于bss段的大小
#include <stdio.h>
#include<stdlib.h>
int a = 0; // 初始化的全局变量 :保存在数据区
int b; //未初始化的全局变量:保存在bss段
int main()
{
int c; // 未初始化的局部变量:保存在栈上
char d[] = "Linux";
//"Linux" 这个字符串常量保存在常量区,不可修改
//d数组保存在栈区,并将Linux\0这个字符串复制到d数组里面,这个数组可以随意修改但是字符串不可以更改
char *e; //保存在栈上
char *p = "Linux Group";
//p保存在栈上
//与上面的数组不同的是,指针是直接指向常量区的,所以不可以做如下操作,否则会发生段错误
//*(p+1)='2';
static int f = 0; // 初始化的静态局部变量:保存在数据区
e = (char *)malloc(sizeof(char)*20); //分配20字节区域保存在堆上
free(e);
return 0;
}
2. main()函数
- 首先,main函数被称为主函数,一个c程序总是从main函数开始执行的
- main函数的返回值是int类型,这刚好与程序最后的return 0;相对应,0是告诉操作系统程序正常退出
- main函数的参数,int argc和char *argv[],第一个参数是命令行中的字符串数,第二个参数是一个指向字符串的指针数组,命令行中的每个字符串都被储存到内存中,并且分配一个指针指向对应的字符串。
4.printf函数
- printf函数的返回值是打印出的字符个数,异常情况下返回负数
- printf函数处理后面的参数是这样的,先从左向右,让参数依次入栈,全部入栈之后,从栈顶开始处理,由此可见,printf函数处理后面的参数,是从右向左处理的
// 该程序的输出结果为1200 60
// 1270 5
#include <stdio.h>
int main(int argc, char *argv[])
{
int a = 10, b = 20, c = 30;
/*
要理解,printf 函数的执行过程:
1.先将printf()函数的参数从左到右读取,然后依次入栈
2.完成之后,从栈顶开始处理。
3.由此可以看出,该函数在处理printf()的参数时,是从右向左处理的
*/
printf("%d %d\n", b = b*c , c = c*2 ) ;
/*
printf() 函数的返回值,是被打印的字符数
*/
printf("%d\n", printf("%d ", a+b+c));
return 0;
}
5.大小端
- 计算机系统中的内存是以字节为单位进行编址的,每一个地址单元都对应着唯一的一个字节,c语言中的char刚好是一个字节,但是,其他类型,就比1个字节大,这就涉及到数据中字节存放顺序的问题,于是就有了大端存储模式和小端存储模式
- 大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中
- 小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中
例如0x1234
大端模式 0x12 0x34
小端模式 0x34 0x12
下面就是两种判断大小端的方法?
#include <stdio.h>
int main()
{
int a=0x12345678;
char *t;
t=(char *)&a;
if(*t == 0x78)
printf("小端\n");
else
printf("大端\n");
return 0;
}
#include <stdio.h>
union test
{
int b;
char ch;
}s;
int main()
{
s.b=1;
//000000000001 0x 00 01
//小端 0x01
//大端 0x00
if(s.ch == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
5.sizeof 与 strlen 的区别
sizeof可以理解为关键字,也可以理解为一元运算符,实际上是获取了数据在内存中所占用的存储空间,以字节为单位来计数,它是在编译的时候就已经计算完的
sizeof表达式有两种
sizeof (类型名)
sizeof 对象 //括号可以省略,也可以带上
它的返回值是一个size_t类型的数,也可以理解为unsigned int 类型,%zu就是以size_t类型输出
// 程序的执行结果为4
// 12
#include <stdio.h>
int main(int argc,char *argv[])
{
int t = 4;
//t-- 之后依然是个整形数据
printf("%lu\n",sizeof(t--));
//全部按单个字符计算,不要忘记'\0'和一些转义字符
printf("%lu\n",sizeof("ab c\nt\012\xal*2"));
//'a' 'b' ' ' 'c' '\n' 't' '\012' '\xa' 'l' '*' '2' '\0' 一共12个字符
return 0;
}
sizeof是计算其所占用的内存空间,对于字符串,一定要注意’\0’这个字符,也要特别注意转义字符
strlen这个函数是计算字符串长度的,与sizeof不同的是
strlen它把’\0’当作一个标志,这个标志之前的所有字符数,就是这个字符串的长度,并不把’\0’算入。
接下来看一道面试题:
#include <stdio.h>
#include<string.h>
int main(int argc, char *argv[])
{
// 默认为sign char
char str[512];
int i;
for (i = 0; i < 512; ++i)
{
str[i] = -1 - i;
}
// 无符号长整型
printf("%lu\n", strlen(str));
return 0;
}
问这个程序输出多少?
输出的是一个字符串的长度,那么我们必须要找到结束的标志,
再看看题目,在for循环中,它是将一个int类型的要存到char型数组里面去,就要截断出一个字节,然后再存进去,
我们知道一个字节是8位二进制数,我们要找到一个数值位0的数,然后存到数组里,就变成了’\0’ ,也就是说后8位必须都为0,而且是第一次出现的
1111 1111 1111 1111 1111 1111 0000 0000
我们可以发现这些数都是负数,所以反推它的补码是上面这个样子
然后还原成源码
1000 0000 0000 0000 0000 0001 0000 0000
-256
所以标志在str[255]处,所以字符串长度位255
6.数组
- 首先看一下数组的其他
非人类表示方法
a[-1][5] = (-1)[a][5]
1[a][5]=a[1][5]
- 理解二维数组在计算机中的按行存储的方式
是下面这样存储的
int a[3][3]
a[0][0] a[0][1] a[0][2] a[1][0] a[1][1] a[1][2] a[2][0] a[2][1] a[2][2]
来两道面试题体会一下(都是问输出结果是什么?为什么?)
#include<stdio.h>
int main(int argc, char *argv[])
{
//二维数组在计算机内部的存储是行存储
int nums[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
printf("%d\n", nums[1][-2] ); //2
//nums[1][-2] 可以看作 nums[1][0] 往左挪2个位置 = nums[0][1] = 2
printf("%d\n", (-1)[nums][5] ); //3
//(-1)[nums][5] = nums[-1][5] = *(*(nums-1)+5) = 3
printf("%d\n", -1[nums][5] ); //-9
//同理:-(1)nums[5] = -nums[1][5] = -nums[2][2] = -9
return 0;
}
// 也可以这样计算a[i][j]的值
//假设数组是a[N][M],按行优先存储 则a[i][j] = a[i*m+j]
//为什么可以这样计算呢?
//这是因为按行优先存储,可以当做一维数组来看,第i行说明前i行所有的元素全满,为i*M个
//在第i行里面,找的是第j个元素,所以和之前的一相加就可以了
#include <stdio.h>
int main(int argc, char *argv[])
{
int a[][2] = {0, 0, 0, 0, 0, 0, 0, 0};
for(int i = 0; i <= 2; i++)
{
printf("%d\n", a[i][i]);
}
//一维的方式存贮的
//0 0
//0 0
//0 0
//0 0
//1.a[0][0] 第一个
//2.a[1][1] 第四个
//3.a[2][2] 第七个
return 0;
}
要明白一维数组的各个名称代表的意义与对应的值
//运行结果(结果每次运行都会不一样)
//0x7ffc3fb886c0
//0x7ffc3fb886c4
//0x7ffc3fb886c0
//0x7ffc3fb886d4
#include <stdio.h>
int main(int argc,char *argv[])
{
int a[5];
printf("%p\n",a); //1
printf("%p\n",a+1); //2
printf("%p\n",&a); //3
printf("%p\n",&a+1); //4
return 0;
}
//1 3 的地址值一样,但是意义不同
//1 数组首元素的地址
//3 整个数组的地址
//2 a[1]的地址
//4 整个数组的之后的地址值
7.linux下的stderr与stdout与windows区别
stderr 与 stdout
stderr是标准错误,stdout是标准输出,两个都是指向显示器
两者默认向屏幕输出。
但如果用转向标准输出到磁盘文件,则可看出两者区别。stdout输出到磁盘文件,stderr在屏幕。
linux下stdout是行缓冲的,只有遇到换行的时候,才会输出到屏幕上
stderr是没有缓冲的,直接输出
window下stdout与stderr都没有缓冲
8.#和##运算符,以及嵌套宏
- 宏,简单理解为替换,是在预处理阶段发生的替换,宏的定义中,括号的意义是很重要的
#include <stdio.h>
#define P(N) N+N
#define Q(A) (A+A)
int main()
{
printf("%d\n",P(2)*P(2));
printf("%d\n",Q(2)*Q(2));
return 0;
}
//第一个输出8
//第二个输出16
- #用来转字符串
- ##记号粘合剂,用来将两个字符连在一起
#define P printf("The square of " #x " is %d.\n",((x)*(x)))
P(2+4)
The square of 2+4 is 36.
a##1 变成了 a1
若果宏嵌套了,#和##会影响输出的结果
#include<stdio.h>
#define YEAR 2018
#define LEVELONE(x) "XiyouLinux "#x"\n"
#define LEVELTWO(x) LEVELONE(x)
#define MULTIPLY(x,y) x*y
int main(int argc , char *argv[])
{
int x = MULTIPLY(1 + 2, 3);
printf("%d\n", x); //7
printf(LEVELONE(YEAR));
//LEVELONE(YEAR)中,第一次的替换中存在#,所以直接输出XiyouLinux YEAR
printf(LEVELTWO(YEAR));
return 0;
}
这里并不是一样的输出,因为存在#和##运算符
1 .当宏中有**#运算符时,参数不再被展开
2 .当宏中有##运算符**时,则先展开函数,再展开里面的参数
9.static
- static全局变量与普通的全局变量的区别
static全局变量只能初始化一次,并且只能在一个源文件中使用
普通的全局变量在整个源程序中都可以使用--------->一个源程序包含多个源文件
- 2.static局部变量与普通的局部变量的区别
static局部变量只能初始化一次,并且static局部变量在编译阶段,变量的空间已经分配,直到程序结束
的时候,才会自动释放,如果不初始化,默认为0
- 3.static函数与普通函数的区别
static函数的作用域仅在本文件中,而且一直使用一个存储区,避免了调用函数是重复的压栈出栈。
普通函数的作用域默认是extern
10.位运算的应用
求数字的二进制数中1的个数
#include <stdio.h>
//用来判断二进制数里面1的个数
int f(unsigned int num);
int main()
{
printf("%d\n",f(2018));
return 0;
}
int f(unsigned int num)
{
unsigned int i;
for ( i = 0; num; i++)
{
num &= (num - 1);
}
return i;
}
2018的二进制表示 11111100010
2017的二进制表示 11111100001
&
2016的二进制表示 11111100000
2015的二进制表示 11111011111
&
1984 11111000000
1983 11110111111
&
11110000000
//每次循环后2018二进制里面都少一个1,
11.const
我们来举几个例子
int * const q = &i; //说明q是const,q的值不能被改变,也就是q指向i是const,不能被改变
*q = 26; //正确的
q++; //错误的
const int *p = &i;//说明不能通过指针p去修改i的值,但是i自身的值是可以变的
*p= 26; //错误的
i = 26; //正确的
p = &j; //正确的
#include <stdio.h>
int main()
{
char y[ ] = "XiyouLinuxGroup", x[ ] = "2018";
char *const p1 = y;
const char *p2 = y;
/*
p1 = x;
p2 = x;
*p1 = 'x';
*p2 = 'x';
*/
return 0;
}
//1,4是错误的
//const在*后面,说明指针变量的值不能改变
//const在*前面,说明指针所指向的地址里面的值不能变
12.3种交换整数的方式
#include <stdio.h>
int main()
{
int a,b;
// 通过一个中间变量
int c=a;
a=b;
b=c;
//加减法
//a = a + b
//b = b + a - b = a
//a = a - a + b = b
a=a-b; b=b+a; a=b-a;
//异或运算
// 用Veen图方便理解
a^=b; b^=a; a^=b;
return 0;
}
前两种不用说,都能明白,最后一种可以用集合来理解
13.二级指针的使用
- 我们来想想,要写一个交换两个数值的函数,传的参数是什么?
是两个数各自的地址,我们要通过指针来交换两个值
那么如果我们要修改指针的值呢,那么我们需要传入指针的地址,就要用到二级指针。
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
char * get_str_01(char *ptr);
void get_str(char **ptr)
{
*ptr = (char *)malloc(18);
strcpy(*ptr,"Xiyou Linux Group");
}
// 我们需要打印的值是str的值,所以我们应该修改str所指的空间里面的东西.
// 原来的程序仅仅是将ptr的指向修改了,并没有改变str所指向的值.
// 所以传值的时候,应该传&str,而且传的是二级指针.
int main(int argc, char *argv[])
{
char *str = NULL;
get_str(&str);
//str=get_str_01(str);
printf("%s\n",str);
return 0;
}
//或者可以这样修改将ptr最后返回给主函数中的str.
char* get_str_01(char *ptr)
{
ptr = (char *)malloc(18);
strcpy(ptr,"Xiyou Linux Group");
return ptr;
}
上面我们是要修改指针所指的空间,改变指针的指向,所以可以用二级指针来操作,也可以用第二种方法,给子函数增加一个返回值就行。
14.返回函数指针的函数
#include <stdio.h>
#include<string.h>
size_t q(size_t b)
{
return b;
}
size_t (*(p(char *str)))(size_t a)
{
printf("%s\n", str);
return q;
}
//函数的返回值是一个q,这是q函数的地址入口,也是q函数的指针.
int main(int argc, char *argv[])
{
char str[] = "XiyouLinuxGroup";
//%lu是无符号长整形
printf("%lu\n", p(str)(strlen(str)));
return 0;
}
该函数的返回值是一个函数指针,返回值为size_t ()(size_t a);
返回函数指针的问题
由于()的优先级大于* ,所以应该将优先级高的括起来:
首先p与后面的char *str结合,那么p是一个函数
其次(p(char str))再与前面的结合,说明该函数的返回值是一个指针
然后,前面的整体再与后面的(size_t a)结合说明该指针指向的是一个返回值为size_t类型的函数
15.结构体的内存对齐
> 为什么存在内存对齐?
-
平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
-
性能原因: 为了访问未对齐的内存,cpu需要作两次内存访问;而对齐的内存访问仅需要一次访问。
结构体的内存对齐是拿空间来换取时间的做法
> 结构体的对齐规则
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
Linux中的默认值为4 - 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
#include <stdio.h>
struct icd
{
int a;
char b;
double c;
};
//16
struct cdi
{
char a;
double b;
int c;
};
//24
int main(int argc,char *arhv[])
{
printf("%zu %zu\n",sizeof(struct icd),sizeof(struct cdi));
return 0;
}
//结构体内存对齐
//嵌套结构体的内存对齐
#include <stdio.h>
struct q
{
char a;
int b;
double c;
};
struct p
{
int f;
struct q j;
double d;
char e;
};
//可以展开为
/*
struct p
{
int f; //占8个字节
struct q
{
char a;
int b; //前两个占8个字节
double c; //最大的8字节
}
double d; //8
char e; //8
};
*/
int main()
{
struct p m;
printf("sizeof = %lu\n",sizeof(m)); //5*8 = 40
return 0;
}
16.c从源程序到可执行程序经过的步骤
1.预处理
linux下,gcc -E用来预处理,不进行编译,汇编和链接
处理头文件
宏定义 宏替换
注释的删除
2.编译
gcc -S 仅编译到汇编语言,不进行汇编和链接
语法分析
词法分析
语义分析
符号汇总
编译完成之后,将生成汇编代码
3.汇编
gcc -c 编译,汇编到目标代码,也就是二进制文件
汇编成机器语言,这一步产生的文件叫做目标文件,二进制格式
4.链接
链接过程将多个目标文件以及所需要的库文件链接成最终的可执行文件
gcc编译的四个步骤
预处理:gcc -E Helloworld.c -o Helloworld.i
编译: gcc -S Helloworld.i -o Helloworld.s
汇编: gcc -c Helloworld.s -o Helloworld.o
链接生成可执行文件: gcc Helloworld.o -o Helloworld