递归函数
函数自己调用自己
void func(){
...
func();
...
}
递归函数必须有结束调节,否则会无止境的递归,最终造成栈溢出,导致程序崩溃。
简易的递归函数可以通过固定的“数学公式“如f(n)=n*f(n-1)
复杂的递归函数则是通过将一个复杂的过程分解成若干个相似的简单步骤来递归实现
递归函数的缺陷
递归函数,是在函数内部不断调用新的自身函数,所以如果递归的深度过深,只有当最深层次的递归函数结束才会返回上一级,这样会占用大量的内存,并且函数执行的效率很低,严重还会导致程序崩溃。
数组
数组是一组内存连续且类型相同变量的合集
一维数组
定义
数据类型 数据名[数据长度]
int arr[10] 代表了arr数组中有十个数据,数据类型均为int
sizeof(数组名) 数组内存大小
sizeof(数组名)/sizeof(数组名[0]/数据类型) 数组中元素的个数 数组长度
初始化
//1.指定初始值
int arr[5] = {1,2,3,4,5};
//2.指定前面元素的值,后面自动补0
int brr[5] = {1,2}; //1,2,0,0,0
//3.{}初始化器中元素个数多于数组长度 警告
int crr[5] = {1,2,3,4,5,6}; //警告
//4.全部初始化为"零"
int drr[5] = {0}; //建议使用
int err[5] = {}; //也是全部初始化为"0" 但建议使用上面一种
int frr[5] = {1}; //不是将所有元素全部初始化1 只有第一个元素为1,其它元素为0
//5.零散指定初始值
int grr[5] = {[2]=1,[4]=5}; // 0 0 1 0 5 指定位置的元素明确的值,没有指定则为0
//6.由初始化器指定数组长度
int hrr[] = {1,2,3,4,5}; //数组长度由{}中元素的个数来决定 必须初始化
//7.没有初始化的数组
int irr[5]; //则数组中元素的值为不确定的垃圾值
数组一旦定义之后就不能再对数组进行初始化和赋值
int arr[5];
arr[5]={1,2,3,4,5};
数组元素的访问
数组名[下标] 每个数组中的元素都有固定的下表 通过下表可以获取数组中的元素
可变长数组
数组的长度一旦确定就不能更改,即数组的长度是一个常量
但定义数组是可以将数组的长度为整数表达式
int n;
scanf("%d",&n);
int arr[n];
可变长数组的初始化不能在定义时进行 要通过搭配循环使用
int n=5;
int arr[n]={1,2,3,4,5}; //这是不对的
for(int i=0;i<n;i++){
scanf("%d",&arr[i]);
}
一维数组作为参数传递
当一维数组作为参数传递时,需要传递数组名和数组长度
void func(int arr[],int len){
}
数组作为参数传递时,修改函数形参的值会修同样修改函数实参的值。
void func(int arr[],int len){
arr[0]=1024;
}
int main(){
int arr[10]={};
func(arr,10);
printf("%d",arr[0]);//arr[0]的值会从0变成1024
}
数组越界
数组下标范围为0到数组长度-1
如果超过这个范围就会造成数组越界
数组越界的现象
1.程序运行成功,结果正确
2.程序运行成功,结果不正确
3.程序运行崩溃,段错误,核心已转储
注意:使用数组时一定要注意数组越界问题
二维数据
二维数组本质上可以认为是一维数组,只不过一维数组中存储的元素也是一维数组
定义
数据类型 数据名[二维长度] [一维长度]
在平面上上可以将二维长度和一维长度分别理解为行和列
二维数组中每个一维数组的长度相同
sizeof(数组名) 整个二维数组所占内存大小
sizeof(数组名)/sizeof(arr[0]) 得到二维长度
sizeof(数组名)/sizeof(arr[0] [0])得到二维数组元素个数
sizeof(数组名[0])/sizeof(arr[0] [0])得到一维长度
初始化
二维数组的初始化也是用{}初始化器进行初始化的
* 二维数组初始化器中本应嵌套{},用于指定二维数组中一行元素的值
~~~C
//1.一个萝卜一个坑 元素个数和数组大小正好一一对应
int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
//2.不用{}指定每一个行的元素时 按照每一行元素进行最大限度填充
int brr[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
//3.补充0
int crr[3][4] = {{1,2,3},{4,5},{6,7,8}}; //{1,2,3,0,4,5,0,0,6,7,8,0}
int drr[3][4] = {1,2,3,4,5,6,7,8}; //{1,2,3,4,5,6,7,8,0,0,0,0}
//4.全部初始化为0
int drr[3][4] = {{0}}; //全部初始化为 建议
int err[3][4] = {0}; //可以
int frr[3][4] = {}; //可以
//5.指定初始化
int grr[3][4] = {[1]={1,2,3},[2][2]=1024,[2][3]=1}; //{{0,0,0,0},{1,2,3,0},{0,0,1024,1}}
//6.由元素的个数决定二维数组长度
int hrr[][4] = {1,2,3,4,5,6}; //2行4列
int irr[][4] = {{1},{2},{3}}; //3行4列
int jrr[][4] = {{1,2,3},{},{}}; //3行4列
二维数组的访问
数组名[二维下标] [一维下表]
可变长数组
数组的长度一旦确定就不能更改,即数组的长度是一个常量
但定义数组是可以将数组的长度为整数表达式
int x;
scanf("%d",&n);
int y;
scanf("%d",&n);
int arr[x][y];
可变长数组的初始化不能在定义时进行 要通过搭配循环使用
int x=5;
int y=6
int arr[x][y]={1,2,3,4,5}; //这是不对的
for(int i=0;i<x;i++){
for(int j=0;i<y;j++)
scanf("%d",&arr[i][j]);
}
二维数组作为参数传递
void func(int (*arr)[],int row,int col){
int (*brr)[col]=arr;
}
int main(){
int arr[10][10]={};
func(arr,10,10);
}
数组的定义补充
如果我们想要定义三维,四维,甚至更高的数组
例如三维数组:数据类型 数组名 [三维长度] [二维长度] [一维长度]
其中二维长度 一维长度必须确定,三维长度可以由元素个数决定
三位数组实际也是一维数组 只不过这个一维数组中的元素均为二维数组
typedef 与数组类型
size_t 实际上是 typedef unsigned long int size_t
简化了无符号长整形的名称 所以数组也可以这样
typedef int ARR[5] //ARR为类型名 是长度为5的一维数组 且类型均为int
ARR a //意思为 int a[5]
ARR b[10] // int arr[10][5]
数组下标
下标运算符中的数据为下标
数组名[下标] 也可以表示为 下表[数组名]
其本质是*(数组名+下表)
指针
指针即内存地址
取址运算符&
C语言中 可以通过&运算符 获取变量的存储地址编号
输出地址编号的格式占位符为%p
int p;
printf("%p",&p);
取值运算符*
C语言中 如果知道一个地址编号 可以用*获得地址编号中存储的数据
int p;
printf("%d",*(&p);
定义指针变量
数据类型 * 变量名
表示为 定义一个变量为指针变量 其储存的内存地址中存储数据的数据类型为定义的数据类型
int *p1;
int a;
p1=&a;
double b;
double *p2;
p2=&b;
注意 连续定义指针变量时需要每个变量前都有*
int *a,*b;
指针变量的初始化
int a;
int *p1=&a;
double b;
double *p2=&b;
如果定义一个指针变量时不确定其具体地址,可以初始化NULL
指针变量的赋值
int a=1024;
int b=2048;
int *p =&a;
p=&b;
指针变量的解引用
如果获得一个内存地址,用*进行取值运算即解引用
int a=5;
int *p=&a;
printf("%d",*p);
野指针
如果一个变量在定义时即没有初始化,也没有明确的赋值,则该指针变量保存的数据为垃圾值,也就是指向了不知名的内存地址,称为野指针
野指针不能进行解引用
野指针进行解引用造成的后果不可确定
1、程序正常运行 结果正确
2、程序正常运行 结果不正确
3、程序崩溃 段错误
注意:编写代码时要避免产生野指针
内存地址
内存 分为虚拟内存和物理内存
在C语言程序中 变量存储的内存地址都是虚拟内存
每一个程序都有自己独立的内存,操作系统负责对其进行编号 编号的起始点为0
在32位操作系统中 每个程序拥有4G的虚拟内存
0x00000000 - 0xFFFF FFFF
即2^32B=4G
在32位操作系统中 sizeof(指针) == 4
在64位操作系统中 每个程序拥有256T的虚拟内存(最少)
2^64B=>256T
0x0000 0000 0000 0000 – 0xFFFF FFFF FFFF FFFF
在64位操作系统中 sizeof(指针) == 8
万能指针
void *p
相同类型的内存地址才可以互相赋值,但万能指针可以存储任意类型的内存地址
任意类型的内存地址可以隐式转换为void *
万能指针可以隐式转换为任意类型的指针 ,有些编译器会有警告,有警告时可以强制类型转换
万能指针不能解引用
int n = 1024;
void*p=&n
*p//错误
万能指针也不支持下标运算
int n = 1024;
void *p=&n
p[1] 也就是*(p+1) 原因为 地址偏移1实际上偏移的是一个数据类型的字节长度
int b=1;
int *a=&a;
*a=*((int *)p);
如果要获取万能指针的数据可以强制转换
指针的算术运算
指针两个最常规的运算
一个是解引用 *
一个是算术运算 ± 整数
对指针加减1 实际上是向后或向前便宜个一个单位的内存字节大小
char* 加一个字节
int * 加四个字节
double* 加八个字节
也就是sizeof(类型)个字节
两个内存相减得到的是相差几个单位的内存长度 而不是总长度
即 (数值相减)/sizeof(类型名)
[]补充
arr[i] 实际上是*(arr+i)
arr[i]=>*(arr+i) =>*(i+arr)=>i[arr]
数组名即数组元素首地址
&arr[0] 首元素地址 int arr[5] &arr[0]=>int *
&arr[0] ==&*(arr+0) == arr
[]下标运算符实际上就是指针偏移后解引用
数组作为参数传递时 传递的就是数组元素首地址 所以需要传递数组长度
数组名 以及对数组名取地址
一维数组
数组名即数组元素首地址
T arr[n]
arr = &arr[0] 类型为 T* arr+1即 偏移sizeof(T)
对数组名取地址 即得到整个数组的地址(数组占多个字节内存地址,用最小的那个地址来表示)
得到的地址和数组名的地址一样
T arr[n];
&arr ==== 整个数组的地址 类型为 T (*)[n] 事实上偏移了 sizeof(T [n])
对数组名取地址 即数组内存地址 也就是数组指针
T arr[n];
T (*p)[n] = &arr;
T (*p)[n]; 数组指针 即指针指向一个一维数组,一维数组中的元素为T类型,数组长度为n
二维数组
int arr[3][5] = {};//二维数组 二维长度为3 二维数组中有3个元素 arr[0] arr[1] arr[2]
//arr[0] arr[1] arr[2] 事实上代表的3个一维数组,且数组长度为5 int [5]
二维数组中元素取地址
int arr[3][5] = {};
arr[0][0] == 一个int类型元素 类型为int
&arr[0][0] == 类型为 int *
&arr[0][0] == &*(arr[0]+0) == &*arr[0] == arr[0] == *(arr+0) == *arr
二维数组名取地址
int arr[3][5] = {};
arr的类型为 int [3][5]
&arr的类型为 int (*)[3][5]
int (*p)[3][5] = &arr;
二维数组作为参数传递
事实上传递的是首元素的地址 (二维数组首元素为数组指针),所以形参列表定义为数组指针的形式
void func(int (*p)[],int row,int col){
int (*arr)[col] = p; //p数组指针不确定数组长度 需要转换为指定长度的 col
}
int arr[3][5] = {};
func(arr,3,5);
字符串
字符串是一种数据类型 C语言中没有为字符串提供一个显式的类型名字
字符串是一串连续的字符以’\0’结尾
字符串在c语言中的两种形态
字面值字符串
在C语言中用“”引用的内容称为字面值字符串
printf("hello world");
scanf("%d",&a);
字面值字符串不能修改
特点
不能修改 一经修改程序会崩溃
连续的字面值会自动合并
相同的字面值字符串在内存中只会存储一份
存储位置:代码段
sizeof(字面值字符串)计算包括结束标志位一起在内的字节长度
字符数组
char s1[5] = {'h','e','l','l','o'}; //s1不能作为字符串 只是字符数组 因为没有'\0'
char s2[5] = {'h','e','l','l'}; //s2作为字符串
用字符数组存储字符串时,一般初始化直接用字面值字符串
char s1[10] = {'h','e','l','l','o'};
char s2[10] = "hello";
char s3[10] = {"hello"};
char s4[] = "hello"; //sizeof(s4) == 6 一定会有'\0'存储空间
特点
可以修改
存储在栈区内
即使字符数组内的内容相同 也可能不是同一个数组
字符数组一定要包括’\0’的空间,不然就不能存储字符串,如果空间不够就会认为其数组越界
字符串的输出
%s 输出字符串的内容
%p 输出字符串的首地址
字符串输出函数:
int puts(const char* str)// 输出字符串并自动换行
字符串的记录
对记录字符串 只需要记录起始位置即可
一般使用char*
char * s="hello"
char str[100]="hello"
char* ps =str;
// 字符指针s和ps本身是不存储字符串的 它是记录字符串的存储位置
字符串操作函数
计算字符串长度
即计算字符串中字符个数(以字节为单位),且不包含’\0’
//c语言标准库提供了函数
#include <string.h>
typedef unsigned long int size_t;
size_t strlen(const char *s);
strlen和sizeof的区别
strlen是C语言标准库string.h中定义的函数 sizeof是C语言标准的操作符
strlen求字符串中字符个数 sizeof求变量或者类型的字节宽度
strlen不包含’\0’ sizeof会计算’\0’的空间
strlen对于字面值字符串,字符数组和字符指针效果是一样的 sizeof对于字面值字符串,字符数组,字符指针意义不一样
字符串函数
//返回dest
//把字符串src的内容拷贝到dest字符串中 连'\0'拷贝
char *strcpy(char *dest,const char *src);
//把字符串src中前n个字符拷贝到dest字符串中 并不保证有'\0'
char *strncpy(char *dest,const char *src,size_t n);
//返回dest
//把字符串src的内容追加到dest字符串末尾 一定会有'\0'
char *strcat(char *dest,const char *src);
//把字符串src的前n个字符拷贝到dest末尾 保证会有'\0'
char *strncat(char *dest,const char *src,size_t n);
~~~
#include <string.h>
//比较s1和s2两个字符串是否相等
//两个字符串相等的条件: 字符个数相等,且相同位置的字符也全部相等
//返回0表示两个字符串相等 -1表示s1字符串小于s2字符串 1表示s1字符串大于s2字符串
//字符串的大小取决于第一个相同位置但字符不同的两个字符的ascii码大小
int strcmp(const char *s1,const char *s2);
//只比较s1和s2字符串前面n个字符
int strncmp(const char *s1,const char *s2,size_t n);
#include <strings.h>
//case忽略大小写比较
int strcasecmp(const char *s1,const char *s2);
int strncasecmp(const char *s1,const char *s2,size_t n);
字符串的读取
需要注意的,只有字符数组才能用于接收读取的字符符
使用%s格式占位符
char str[100]={};
scanf("%s",str);
遇到空白字符就会结束读取 无法读取有空白字符的字符串
使用gets函数(不建议使用)
char *gets(char *s);
//能够读取空白字符
//这个函数会读取缓冲区中所有的字符,直到遇到\n结束 可能会导致数组越界 比较危险 C99中把这个函数删除了
//如果之前有输入过数据,那么缓冲区会有\n 直接返回
使用fgets函数
读取缓冲区中一行数据,遇到’\n’结束,如果空间足够,连同’\n’一起读取
* 即使缓冲区内容多于n个,最多读取n-1个字符,保证最后有一个’\0’
char *fgets(char *s,size_t n,FILE *stream);
//stream参数给 stdin
//n参数 字符数组长度
char str[100] = {};
fgets(str,100,stdin); //输入不够99个字符,则会连同'\n'一起读取到str中 如果超过99,则只会读取前面99个字符,并且最后加一个'\0'
//能够保证一定会有'\0'