本文是c语言的进阶学习,需要先了解一些预备知识:程序是如何运行的?
变量的本质是符号,或者说是一块内存空间的别名。
因此无论什么变量,变量都是指符号,其余部分都是类型,比如:
int a; //int型变量a
int *a; //int* 型变量a,无大小限制,需要指定结束符
int a[10]; //a是int[10]类型
//a是struct test类型; pA是struct test *类型;b是struct test [10]类型
struct test{
int h;
}a,*pA,b[10];
首先要理解变量是个符号,
普通变量指代的是一个内存块中的值,因此内存块是固定的,值可变;
指针变量指代的是一个指针,因此内存地址是可变的。
因此与其理解变量存储的是值或指针,不如理解为变量是内存的接口,内存的内容可以变,但接口不变。
接口通常是概念化的存在。
生命周期
指的是:变量占用内存的时间,只要在内存里,就是活着的。
内存模型:
静态存储区,存放全局变量和静态变量。
动态存储区,堆和栈。
栈,存放函数的参数、返回值和局部变量。
堆,存放程序员管理的变量。
生命周期:
静态变量,包括静态局部变量和静态全局变量,生命周期是整个程序运行期间。
作用域:
静态局部变量,作用域是函数内,但生命周期是整个程序运行区间。
初始化:
全局变量,程序的运行开始前,在main函数之前;
局部变量,程序运行到该语句时。
全局变量
同一程序中,所有函数外的变量,是全局变量;
不同进程中,所有进程外的变量,是环境变量。
内部函数用static
声明,外部函数用extern
声明,默认情况下是extern
。
因此声明函数时加extern
和不加extern
效果是一样的。
全局变量也是如此,只是全局变量在外部文件引用时,也要加上extern
。
先说变量
,变量的本质是一块内存块
的别名,就是给一个内存块取了别名,操作它时会读写内存块中的值,因此变量是内存块和值得组合。
那么指针
,指针是一个内存块的首地址,如果是void *
,那么就只是一个指针大小,如果是int *
,那么就是一个32位的内存块,因此指针变量是内存首地址和内存大小的组合。
理解指针
验证指针的操作方法:
#include
int main()
{
int a[3]={1,2,3};
printf("a:%p\n",a);
printf("&a:%p\n",&a);
printf("&a[0]:%p\n",&a[0]);
printf("&a+1:%p\n",&a+1);
printf("&a[0]+1:%p\n",&a[0]+1);
printf("a[3]:%p\n",&a[3]);
printf("test:%d\n",&a+1-&a);
return 0;
}
a:0x7ff7b1963f0c
&a:0x7ff7b1963f0c
&a[0]:0x7ff7b1963f0c
&a+1:0x7ff7b1963f18
&a[0]+1:0x7ff7b1963f10
a[3]:0x7ff7b1963f18
test:1
可以看到,对于数组:
a=&a[0]=&a,都是数组首地址;
a+1=&a[0]+1,都是int指针,每次+1前进4个字节;
&a+1,是int[3]指针,每次前进4*3=12个字节,执行后指向数组最后一个元素的地址。
概念不懂有一半原因是名字取的不好。
指针数组是:存放指针的数组;
数组指针是:存放数组的指针。
实体都是最后一个词。
指针数组:
int *p[8];
数组指针:
int (*p)[8];
概念不懂的另一个原因是,结合律和顺序不一致,这要写成int [8] *P,不是秒懂!
由于结合律的原因,在int *p[8];
中,p会优先和[]结合,因此int *p[8];
是存放8个int型指针的数组
;而int (*p)[8];
中,使用括号让*和p优先结合,因此int (*p)[8];
就是存放int[8]的数组的指针
(首地址)。
二维数组是使用指针数组实现的。对于a[m][n]
,a[0]
代表的是存放第一行数组的指针的指针,因此解引用*a[0]
就获得了第一行数组的指针,那么*((*a[0])+0)
就是第一个元素的值。
二维数据获取首地址的四种方法:
#include
int main()
{
int arr[4][4]={0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
int i=0;
printf("arr+i:%p\n",arr+i);
printf("arr[i]:%p\n",arr[i]);
printf("*(arr+i):%p\n",*(arr+i));
printf("&arr[i]:%p\n",&arr[i]);
return 0;
}
arr+i:0x7ff7b3bcbed0
arr[i]:0x7ff7b3bcbed0
*(arr+i):0x7ff7b3bcbed0
&arr[i]:0x7ff7b3bcbed0
arr+i
是数组指针,+1后指针前进一位;
arr[i]
是指针数组,访问第i个元素,存储的是第i行数组的地址;
*(arr+i)
是数组指针,访问第i个元素,解除引用,得到值,存放的是第i行数组的地址;
&arr[i]
=arr[i]
,是指针数组。
指针函数:返回值带指针的函数;
int *fun()
函数指针:指向函数的指针。
int (*fun)()
所有指针双名词组合的词,都是后面的词是实体。
指针函数没啥好说的,返回值是指针类型。
那么函数指针,作为接口,可以传入各种同类型的函数,使用方法如下:
#include
int (*fun)(int a,int b);
int add(int m,int n)
{
return m+n;
}
int main()
{
fun=add;
printf("add:%d\n",fun(3,4));
return 0;
}
add:7
值传递:传递值过程中,会创建一个新值,新值的地址和原值地址不一样。
地址传递:传递值的地址,因此引用的是同一个数据,且传递的大小只是指针大小。
如何理解递归?
#include
int fibonacci(int n) //1,假设地址0x7ff7b3bcbed0
{
if (n == 1) {
return 0;
} else if (n == 2) {
return 1;
} else {
return fibonacci(n-1) + fibonacci(n-2); //2,假设地址0x7ff7b3bcd3b4
}
}
int main()
{
printf("%d\n",fibonacci(5));
return 0;
}
函数在内存中是顺序存储的,因此在执行过程中,首先执行到1(地址:0x7ff7b3bcbed0),然后继续执行,执行到2(0x7ff7b3bcd3b4),此时又跳回地址1(0x7ff7b3bcd3b4),反复如此,只是参数每次都不一样,然后一直执行到边界值1或2就会退出了。
char、int、指针、bool这些都是存储一个值用的,那么想存储多个值怎么办?
可以使用数组,但是数组只能存储相同类型的值,那么想存储不同类型的值怎么办?
可以用结构体。
单值存储:char、int、指针、bool;
多值存储:数组;
自定义数据类型:struct、union、enum、位域。
结构体
是c语言中用户自定义的数据类型,使用方法如下:
#include
int main()
{
struct Mytype{
int a;
char b;
}hello;
hello.a=3;
printf("hello.a=%d\n",hello.a);
printf("hello.b=%d\n",hello.b);
printf("hello.b=%c\n",hello.b);
hello.b='f';
printf("hello.a=%d\n",hello.a);
printf("hello.b=%d\n",hello.b);
printf("hello.b=%c\n",hello.b);
printf("sizeof:%d\n",sizeof(hello));
return 0;
}
hello.a=3
hello.b=0
hello.b=
hello.a=3
hello.b=102
hello.b=f
sizeof:8
为了节省空间,c语言还存在共用体这种数据类型,也是用户自定义数据类型,但是与结构体不同的是,结构体会将所有成员变量分配空间,而共用体只会分配一份成员变量中占用内存最大的空间。
联合体
的使用方法如下:
使用方法如下:
#include
int main()
{
union Mytype{
int a;
char b;
}hello;
hello.a=3;
printf("hello.a=%d\n",hello.a);
printf("hello.b=%d\n",hello.b);
printf("hello.b=%c\n",hello.b);
hello.b='f';
printf("hello.a=%d\n",hello.a);
printf("hello.b=%d\n",hello.b);
printf("hello.b=%c\n",hello.b);
printf("sizeof=%c\n",sizeof(hello));
return 0;
}
hello.a=3
hello.b=3
hello.b=
hello.a=102
hello.b=102
hello.b=f
sizeof=4
注意,此时b会覆盖a的值,因为hello里永远只保留一个值。
在日常生活中,常有一些概念是用有序数列组成的,比如一周有7天,分别叫星期一、星期二、…,为了方便这些概念性有序数列的归纳,c语言制作了枚举类型,其性质是成员自增+1。
枚举
是定义了一系列概念,这些概念都是用数字表示,定义方法如下:
enum Days{
Mon=1,Tues,Wednes,Thurs,Fri,Sar,Sun
};
这个概念就相当于定义了:
#define Mon 1
#defien Tues 2
...
因此枚举和宏定义是可以互换的,只是枚举强化了封装的概念并加入了类型检测,而宏是散装的且没有类型检测。
枚举的使用方法如下:
#include
enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN };
void main()
{
enum DAY yesterday, today, tomorrow;
yesterday = TUE;
today = (enum DAY) (yesterday + 1); //类型转换
tomorrow = (enum DAY) 3; //类型转换
//tomorrow = 3; //错误
printf("%d %d %d \n", yesterday, today, tomorrow); //输出:2 3 3
}
众所周知,数据在内存中是按字节大小读写的,但在嵌入式中,内存空间很奢侈,因此需要按位的大小来存储数据。
位域
,是用户自定义的一种数据结构,允许用户将特定大小的位数组成一个域,并含有域名,这样用户就可以按域名来操作位域了。
位域的定义方式如下:
#include
struct MyType{
char a:6;
char b:2;
char c:7;
}data;
int main()
{
printf("sizeof:%d\n",sizeof(data));
return 0;
}
2
其原理是:
当相邻位域的类型相同时,如果其位宽之和小于该类型所占用的位宽大小,那么后面的位域紧邻前面的位域存储,直到不能容纳为止;如果位宽之和大于类型所占用的位宽大小,那么就从下一个存储单元开始存放。
1.引用文件
文件的引用方式有:
#include<文件名>
#include"文件名"
区别在于,#include<文件名>
只会在系统头文件目录中查找文件,#include"文件名"
先在系统头文件目录中查找,再去当前目录下查找。
2.宏定义
宏只是文本替换
通过井undef可以指定宏的作用域,如果不指定,作用域就是全局。
#define pi 3.14
...
#undef
带参数的宏替换
3.条件编译
#if 常量表达式
程序段;
#else
程序段;
#endif
4.#pragma指令
#pragma
指令的作用是设置编译器状态。
//在编译期间输出信息
#pragma message("我是输出")
//只编译一次
#pragma once
//编译头文件到此为止,后面的无需再编译了
#pragma harstop
//设置字节对齐
#pragma pack(2)
//设置警告信息
#pragma warning(disabel:M N)
#pragma warning(ocne:H)
#pragma warnong(error:K)
goto语句也称为无条件转移语句。goto语句只能在函数内部进行转移,不能跨越函数。
goto 标号;
其中,标号使用":"进行标识。
样例:
#include
int main()
{
int n=100;
int num=0;
loop:
num+=n;
if(n<100 && n>0)
goto loop;
return 0;
}
#include
int main()
{
int i;
char arr_s[]={"Hello World!"};
char arr_c[]={'H','e','l','l','o',' ','W','o','r','l','d','!'};
printf("sizeof(arr_s):%d\n",sizeof(arr_s));
printf("sizeof(arr_c):%d\n",sizeof(arr_c));
return 0;
}
sizeof(arr_s):13
sizeof(arr_c):12
可以看到,字符串常量初始化,会自动加上\0
,而字符初始化不会带上\0
。
assert是if…else的一种替代方法,其作用及优点
是:
1.简化判断的写法;
2.如果表达式时候假(即为0),那么他先输出一条错误,然后调用abort函数终止程序;
3.可通过宏定义进行屏蔽和启用,易于将开发和发布分开。
缺点是:
1.影响性能;
断言的使用方法:
assert(表达式);
表达式可以是常量、表达式、函数等。
屏蔽断言的方法:
#define NDEBUG
编译器通常不为普通const常量分配存储空间,而是将它保存在符号表中,这使它成为一个编译期间的常量,没有了存储与读内存的操作,它的效率也很高。
首先,如下两种方式是等价的:
const int n=5;
int const n=5;
所以,如下两种方式也是等价的,都是指针常量
(指向常量的指针):
const int *p;
int const *p;
如果const位于*
的左侧
,那么const就是用来修饰指针所指向的变量的,常量指针
,即指针指向常量
;如果const位于*
的右侧
,那么const就是修饰指针本身的,常量指针
,即指针本身是常量
。
所以,常量指针(常量性质的指针)定义方法如下:
int *const a=&b;
指向常量的常量指针如下:
const int *const a=&b;
#define
是预处理指令,用于文本替换,将前边的文本替换为后边的文本。
typedef
用于给关键字取别名,相应的过程是在编译期间完成的。
#define INT int
typedef short SHORT;
因此预处理都不需要分号作为语句结束
而编译期间的语句都需要分号结束
在给关键字取别名的时候,建议使用typedef,因为#define会出问题:
#define PINT int*
typedef short* PSHORT;
PINT a1,a2;
PSHORT b1,b2;
我们的本意是定义整形指针a1和a2,但这里展开后是这样的int * a1,a1;
,因此只定义了a1是指针类型;
而b1和b2则能如期定义出我们想要的指针类型。
typedefine的用法:
//给数组取别名
typedef int arr[4];
arr a1,a2;//此时a1和a2的类型是int[4]
//给函数指针取别名
typedef int (*pfun)(int n);
pfun a;
a=b;
#include
void *realloc(void *mem_address,unsigned int newsize);
按newsize大小重新分配一款内存空间,并释放原来空间,返回新内存空间的地址,失败返回NULL。
#include
void *malloc(unsigned int num_bytes);
分配指定字节数的内存空间,不进行初始化,并返回内存空间地址,失败返回NULL。
#inlcude<stdlib.h>
void *calloc(unsigned n,unsigned size);
分配n个长度为size的连续内存空间,自动初始化内存空间为0,返回内存空间地址,失败返回NULL。
指针的两种用法:
1.作为游标,读取已存在内存的数据;
2.作为动态内存的索引。
如果要作为动态内存的索引,切记要初始化,以下是示例:
typedef struct myStack{
int value;
int *base;
int *top;
}stack,*pStack;
/*
//错误示例,此时会因为没有开辟内存空间,导致内存访问错误。
pStack a;
a->base=(int *)malloc(sizeof(int)*100);
*/
//正确的做法是:
pStack a=(pStack)malloc(sizeof(stack));
a->base=(int *)malloc(sizeof(int)*100);
指向指针的指针有如下作用:
1.二维数组;
2.指针数组,如字符串数组;
3.通过传参修改指针的指向。
通过指针传入数据,局部变量会拷贝一个指针副本,可以改变指针所指地址空间的内容,但不能改变指针本身,换句话说,不能改变入参,如果想改变入参,有两种方式:
1.使用指针的指针;
2.使用C++的引用。
在操作系统内核中,有一套函数库,叫内核库,
这套内核库,只能在内核态状态下使用,
内核的职责包括管理所有硬件设备,
因此当用户想调用设备时,比如向屏幕输出文字,就需要通过系统调用函数,调用内核函数,再访问屏幕。
因此系统调用是操作系统暴露给用户的内核操作接口,API。
所有的操作系统在其内核里都有一些内建的函数,这些函数可以用来完成一些系统级别的功能。Linux系统使用的这样的函数叫做“系统调用”,英文是systemcall。这些函数代表了从用户空间到内核空间的一种转换,例如在用户空间调用open函数,则会在内核空间调用sys_open。一个已经安装的系统的支持的所有的系统调用可以在/usr/include/bits/syscall.h文件里面看到。
在计算机中,系统调用(system call),又称为系统呼叫,指运行在使用者空间的程序向操作系统内核请求需要更高权限运行的服务。
系统调用提供了用户程序与操作系统之间的接口。(即系统调用是用户程序与内核交互的接口)
系统调用,我们可以理解是操作系统为用户提供的一系列操作的接口(API),这些接口提供了对系统硬件设备功能的操作。
可以举个例子,我们最熟悉的 hello world 程序会在屏幕上打印出信息,程序中调用了printf() 函数,而库函数 printf 是将需要打印的信息输出到屏幕这个硬件设备上。我们知道,对于所有硬件设备的操作都需要驱动程序,而驱动程序是由操作系统内核实现 的。这也就意味着我们的printf()函数最终需要调用Linux内核的相关函数来操作屏幕这个设备,而这些函数就是系统调用。事实 上,printf()库函数的实现里最终会调用系统调用 write() 。
另外,创建进程fork(),vfork(),open、read、sbrk都是系统调用。
库函数可以理解为是对系统调用的一层封装。系统调用作为内核提供给用户程序的接口,它的执行效率是比较高效而精简的, 但有时我们需要对获取的信息进行更复杂的处理,或更人性化的需要,我们把这些处理过程封装成一个函数再提供给程序员,更 方便于程序猿编码。譬如在接下来的学习过程中,我们会学习read(int fd, char *buf, int size)这个系统调用,这个函数是从某个 文件(由fd)标志中,读取最多不超过size个字节的数据并存放到buf中去,在这个系统调用中我们只能指定读N个字节的数据,而 对于想一次读取一行这样的需求则可以调用库函数fgets()来实现,这在《C Primer Plus》的文件/IO中讲到,当然在《C Primer Plus》里提到的所有函数都是库函数。
例如: 典型的C函数库printf、fopen、fread、 malloc
数据类型的大小是不同的,如果不按照字节对齐的方法存储,CPU会消耗多个指令周期才能找到数据;
字节对齐是为了让CPU能在一个指令周期内找到数据;
字节对齐的原则:
对于第一条准则,编译器会自己处理,所以我们只关注2和3;
struct A{
char a;
int b;
}
成员a的起始地址是0,占用大小至少和下一个成员一样,即a占用4字节;
成员b的起始地址是4,占用大小是自己的大小,即4字节;
因此结构体A占用大小是8字节。
struct B{
char t1;
A t2;
char t3;
}
结构体B内嵌了结构体A,应该把A中的数据展开,那么结构体B的成员是char,char,int,char。
分别占用空间为:
第一个char的首地址是0,占用一个字节,但是根据第一条原则,结构体A的首地址应该是int的整数倍,因此第一个char实际上占用的是4字节,
第二个char占用4字节;
第三个int占用4字节;
第四个char占用1字节。
一共13个字节,但是不符合第三条原则,
满足第三条原则,需要是int的整数倍,那么大于13字节的最小整数倍,是16。
因此结构体B共占用16个字节。
以下是一些规则:
char a[]="我是好人";
char a[10]="我的世界";
char *a;
char a[]; //error
char a[5]; //error
注意,字符串数组被初始化后是不能被赋值的:
#include
#include
int main()
{
char a[10]={};
a="hello"; //error,不能被赋值。
printf("%s\n",a);
return 0;
}
但是字符串初始化后,可以通过指针来改变内容:
#include
#include
int main()
{
char a[10]="1234567";
char *b;
b=a;
b[1]='a';
printf("%s\n",a);
return 0;
}