本文是c语言的进阶学习,需要先了解一些预备知识:程序是如何运行的?
生命周期和作用域
生命周期 指的是:变量占用内存的时间,只要在内存里,就是活着的。
内存模型: 静态存储区,存放全局变量和静态变量。 动态存储区,堆和栈。 栈,存放函数的参数、返回值和局部变量。 堆,存放程序员管理的变量。
生命周期: 静态变量,包括静态局部变量和静态全局变量,生命周期是整个程序运行期间。
作用域: 静态局部变量,作用域是函数内,但生命周期是整个程序运行区间。
初始化: 全局变量,程序的运行开始前,在main函数之前; 局部变量,程序运行到该语句时。
全局变量 同一程序中,所有函数外的变量,是全局变量; 不同进程中,所有进程外的变量,是环境变量。
内部函数和外部函数
内部函数用static 声明,外部函数用extern 声明,默认情况下是extern 。 因此声明函数时加extern 和不加extern 效果是一样的。 全局变量也是如此,只是全局变量在外部文件引用时,也要加上extern 。
指针
先说变量 ,变量的本质是一块内存块 的别名,就是给一个内存块取了别名,操作它时会读写内存块中的值,因此变量是内存块和值得组合。 那么指针 ,指针是一个内存块的首地址,如果是void * ,那么就只是一个指针大小,如果是int * ,那么就是一个32位的内存块,因此指针变量是内存首地址和内存大小的组合。
理解指针 验证指针的操作方法:
#include<stdio.h>
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<stdio.h>
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<stdio.h>
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<stdio.h>
int fibonacci(int n)
{
if (n == 1) {
return 0;
} else if (n == 2) {
return 1;
} else {
return fibonacci(n-1) + fibonacci(n-2);
}
}
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<stdio.h>
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<stdio.h>
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 <stdio.h>
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;
printf("%d %d %d \n", yesterday, today, tomorrow);
}
众所周知,数据在内存中是按字节大小读写的,但在嵌入式中,内存空间很奢侈,因此需要按位的大小来存储数据。 位域 ,是用户自定义的一种数据结构,允许用户将特定大小的位数组成一个域,并含有域名,这样用户就可以按域名来操作位域了。 位域的定义方式如下:
#include<stdio.h>
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语句只能在函数内部进行转移,不能跨越函数。
goto 标号;
其中,标号使用":"进行标识。 样例:
#include<stdio.h>
int main()
{
int n=100;
int num=0;
loop:
num+=n;
if(n<100 && n>0)
goto loop;
return 0;
}
字符数组
#include<stdio.h>
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
assert是if…else的一种替代方法,其作用及优点 是: 1.简化判断的写法; 2.如果表达式时候假(即为0),那么他先输出一条错误,然后调用abort函数终止程序; 3.可通过宏定义进行屏蔽和启用,易于将开发和发布分开。
缺点是: 1.影响性能;
断言的使用方法:
assert(表达式);
表达式可以是常量、表达式、函数等。
屏蔽断言的方法:
#define NDEBUG
const
编译器通常不为普通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 是预处理指令,用于文本替换,将前边的文本替换为后边的文本。 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;
typedef int (*pfun)(int n);
pfun a;
a=b;
内存管理
#include<stdlib.h>
void *realloc(void *mem_address,unsigned int newsize);
按newsize大小重新分配一款内存空间,并释放原来空间,返回新内存空间的地址,失败返回NULL。
#include<stdlib.h>
void *malloc(unsigned int num_bytes);
分配指定字节数的内存空间,不进行初始化,并返回内存空间地址,失败返回NULL。
#inlcude<stdlib.h>
void *calloc(unsigned n,unsigned size);
分配n个长度为size的连续内存空间,自动初始化内存空间为0,返回内存空间地址,失败返回NULL。
|