1.函数
函数的作用:重复使用代码。
(1)调用函数
注意使用sin()、cos()、sqrt()、pow()、fabs()等数学函数在代码中,需要加入头文件math.h;在编译时加上链接库-lm。
(2)定义函数
返回类型、函数名、参数列表、函数体
<1>从函数中返回值
return表示停止函数执行,return可以在停止函数执行同时返回一个值。
返回值可以传递给变量、函数、或者丢弃。
- 返回值有时存在两种情况:合法值和非法值。
如果有非法值的情况,通常使用一些特定的值指代特殊情况。例如:数组下标只能是0和正数,可以使用-1来表示找不到元素的情况。
<2>没有返回值的函数
void 函数(参数列表)
不能使用带值的return;可以没有return;调用时没有返回值
(3)有关函数的说明
- main()是一个特殊的自定义函数。
- 定义函数不会执行函数体代码,只有调用函数时,才是真正执行。
- 函数名也是标识符,也要遵守与变量一样的命名规则。
- 函数没有参数时,参数列表可以为空,也写入关键字void表示为空。
- 函数没有返回值时,返回类型必须写void。
(4)main()参数与返回值
<1>参数
argc是命令与参数数量。argv是命令和参数组成的字符串数组。argv[0]是命令本身。其余是参数。
int main(int argc,char* argv[])
或者:
int main(int argc,char** argv)
<2>main()返回值
main()返回值是与调用程序交互的,返回程序执行状态。通常0表示执行成功,非零表示执行失败。
在终端执行程序后,接着执行
echo $?
可以看到返回值。
(5)函数原型
<1> 函数原型是什么
函数头以;
结尾,构成函数原型。
<2> 函数原型怎么用
函数原型通常放在头文件里面或者调用它的函数的前面。
<3> 函数原型有什么用
提前告诉编译器使用的函数基本信息(函数名、参数个数和类型、返回类型)。
<4> 函数前置声明
通常把main()
放在代码最前面便于阅读,但是这样会导致编译时因为找不到main()内部调用函数而错误或警告。在main()
前加上函数原型可以解决这类问题,称为函数前置声明。
void func();// 函数前置声明
void main(){
func();
}
void func(){
printf("Hello World\n");
}
(6)指针与函数
<1>函数名
函数名与数组名一样表示的是地址,不同的是函数名是执行函数代码的起始位置,数组名是数组第一个元素的地址。
void func(){ }
void main(){
printf("func=%p\n",func);
printf("func=%p\n",&func);
}
直接使用函数名
func
和取函数地址&func
获取的值是相同的。
<2>函数指针
函数指针是指向函数的指针变量,即本质是一个指针变量。
数组名即数组的指针,函数名也是函数的指针。
-
定义:
类型说明符 (*指针的变量名)(参数); void (*fptr)();
指针名和指针运算符外面的括号改变了默认的运算符优先级。如果没有圆括号,就变成了一个返回整型指针的函数的原型声明。
-
赋值
把函数的地址赋值给函数指针,可以采用下面两种形式:fptr = func; fptr = &func;
函数名即函数地址。这两种赋值方式完全一样。
-
调用
函数指针调用也可以采用下面两种形式:(*fptr)(); fptr();
这两种调用方式完全一样。第二种格式看上去和函数调用无异。但是有些程序员倾向于使用第一种格式,因为它明确指出是通过指针而非函数名来调用函数的。
void func3(int a);
void main(){
void (*fp)(int a);//定义函数指针
fp=func3;//给指针赋值(或者:fp=&func3)
(*fp)(1);//函数指针解引用,相当于调用func3(1)
fp(2);//作用和上面一样,func3(2)
printf("&func3 = %p\n",&func3);// 函数的地址
printf("fp = %p\n",fp);// 函数指针的值(和上面的一样!!!)
}
void func3(int a){
printf("%d\n",a);
}
<3>指针函数
- 指针函数是指带指针的函数,即本质是一个函数,函数返回类型是某一类型的指针。
- 当一个函数声明其返回值为一个指针时,实际上就是返回一个地址给调用函数,以用于需要指针或地址的表达式中。
(7)回调函数
函数指针就可以作为函数的参数,这称为回调函数。
<1>组成元素
- 主函数:相当于整个程序的引擎,调度各个函数按序执行
- 回调函数:一个独立的功能函数,如写文件函数
- 中间函数:一个介于主函数和回调函数之间的函数,登记回调函数,通知主函数,起到一个桥梁的作用。
<2>回调函数执行的流程
- 主函数需要调用回调函数
- 中间函数登记回调函数
- 触发回调函数事件
- 调用回调函数
- 响应回调事件
<3>理解
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。
- 电话号码就叫
回调函数
; - 把电话留给店员就叫
登记回调函数
; - 店里后来有货了叫做
触发回调事件
; - 店员给你打电话叫做
调用回调函数
; - 到店里去取货叫做
响应回调事件
<4>例子
qsort
int cal_array(int* arr,int n,int (*pf)(int,int)){
int res=*arr;
while(--n){
res=(*pf)(res,*++arr);//pf(res,*++arr)
}
return res;
}
int add(int a,int b){
return a+b;
int main(){
int arr[]={2,3,6,78,8,3};
printf("sum=%d\n",cal_array(arr,6,add));//函数指针,函数名为函数地址
2.指针
(1)指针的定义
- 指针是保存变量地址的一个变量。普通变量的值是实际值,指针变量的值是变量的地址。
- 变量名前加上&,表示获取变量的地址。变量地址通常使用16进制表示,使用%p或者%P打印地址。
(2)指针的使用
<1>定义指针
类型* 指针变量;
指针变量只能使用同类型变量的地址赋值。也可以直接初始化。
变量必须赋值后才能使用,指针也是必须赋值后才能使用。
<2>解引用
指针的强大之处可以直接操作储存地址里面的数据。这种操作称为解引用。使用时需要在指针前加上*星号。
注意:这里的*与声明指针的含义不一样,与乘号也不一样。
-
访问变量两种方式:一是通过变量名直接访问,而是指针解引用访问。
(3)指针与函数
<1>值传递
<2>指针/地址传递
注意
<1>函数内部改变函数外部定义的局部变量必须满足两个条件:
*指针参数
*解引用
<2>指针在函数中的应用
指针在函数中有这两种应用,一种是即作为输入又作为输出;另一种只作为输出。,可以返回多个值。
(4)指针运算
<1>算术运算
- 加减
+
、-
指针与整数相加:表示指针指向下个变量。//p+i
指针与整数相减:表示指针指向上个变量。//p-i
指针与指针相减:两个指针的元素间隔个数。//p-q - 自增自减
++
、--
自增、自减只能是变量使用,数组,常量不能使用。
int arr[]={100,101,102,103,104,105};
for(i=0;i<5;++i){
printf("%p\n",arr++);//错误。
}
(1)*p++/*p--
自增自减++、--优先级高于解引用*
等价于:
*p;
p=p+1;
(2)*++p/*--p
等价于:
p=p+1;
*p;
(3)++*q/--*q
先运算*,获取q指向的值,运算++/--,q指向的值自加/自减。
++/--一定会改变变量的值,前两种改变指针的值,后一种改变元素的值。
<2>单位长度
数组中的元素地址线性递增。指针的加1减1,地址并非加1减1。而是根据类型的不同增加不同的字节。
int iarr[] = {1,2,3,4,5,6};
int* p = iarr;
for(int i=0;i<5;++i){
printf("%p\n",p++);//
}
char carr[] = {1,2,3,4,5,6};
char* q=carr;
for(int i=0;i<5;++i){
printf("%p\n",q++);
}
(5)指针类型
<1>指针大小
无论指针是什么类型,所有指针的大小都是一样的,都是地址的大小。64位的一般是8个字节,32位的一般是4个字节。
<2>指针类型转换
- 指向不同类型的指针不能直接相互赋值(特例void*),需要强制类型转换。
- 指针类型转换没有改变指针内的地址,也没有改变指针指向的值,只是改变了移动的单位长度。
char* str = "abcdef";
int* p=(int*)str;
p++;
char* q = (char*)p;
printf("%c\n",*q);//e
<3>void类型的指针
void*是一种很特别的指针,表示指向未知类型的指针,并不指定它是指向哪一种类型的数据,而是根据需要转换为所需数据类型。 使用前一定要转换
int n = 0;
int* p = &n;
void* q = p;
printf("%d\n",*q);//错误,invalid use of void expression
int* k = (int*) q;
printf("%d\n",*k);//正确
(6)0地址
0地址是内存中不能访问的地址。在C语言中,标准库定义NULL表示0地址。
int *p = 0;
printf("%d\n",*p);
作用:
(1)指针没有初始化
(2)返回指针无效
(7)指针的作用
- 较大数据结构体传入时做参数。
- 传入数组后,对数组做操作
- 函数需要多个返回值时,作为返回值参数
- 动态申请内存
- 避免使用未初始化指针、空指针和野指针。
(8)变量指针和数组指针
<1>变量指针
变量指针:指向单个变量的指针。
int n = 10;
int *p;
p = &n; // p指针指向变量
printf("*p = %d\n",*p);
<2>数组指针
数组指针:指向数组的指针。
int arr[] = {1,2,3,4,5,};
p = arr;// p指针指向数组
printf("*p = %d\n",*p);
printf("*(p+1) = %d\n",*(p+1));
(9)数组指针 vs 指针数组
<1>数组指针
指向一个数组的指针称为数组指针。
<2>指针数组
指针是一个类型,也可以组成一个数组,这样的数组称为指针数组。
int a = 1;
int b = 2;
int c = 3;
int* p[] = {&a,&b,&c};
for(int i=0;i<3;++i){
printf("%d\n",*p[i]);//1,2,3
}
for(int i=0;i<3;++i){
printf("%d\n",**(p+i));//1,2,3
}
[ ]的优先级高于*,那么p先和[]结合,说明这是一个数组。再和int*结合,说明这个数组里的每个元素都是一个指针,每个元素都能保存一个地址。
(10)常量指针 vs 指针常量
<1>常量指针
常量指针:const int *p
也可以写作int const *p
,const
修饰的是*p
,所以*p
是常量,表示p
指向的地址里的值不可修改,也就是说,*p
里的值不能再重新赋值了,但是可以修改p
指向的地址。(即不能通过p来修改p指向的值,但是可以直接修改修改指向的值)
int a = 10;
int b = 20;
const int *p = &a;
*p = 100; // 错误
a=100;//直接修改指向的值
printf("%d\n",*p);//100;
p = &b; // 可以
<2>指针常量
指针常量:int * const p
,const
修饰的是p
,所以p
是常量,表示p
指向的地址不可修改,即p
不能再指向别的地方了,但是可以修改p
指向的这个地址里的值。
int a = 10;
int b = 20;
int * const p = &a;
p = &b; // 错误
p++;//错误
*p = 100; // 允许
a=1000;
printf("%d\n",*p);//1000
<3>常量指针常量
常量指针常量:const int * const p
,p
是int*
类型,两个const
分别修饰了p
和*p
, 所以p
和*p
都是常量,表示p
指向的地址不可修改,同时p
指向的地址里的值也不可修改。
int a = 10;
int b = 20;
const int *const p = &a;
p = &b; // 错误
*p = 100; // 错误
*
之前的const
修饰指向的变量,*
之后的const
修饰指针。
(11)二维指针的理解
二维指针与一维指针一样都是保存地址的变量。
- 示例1:
int main(){
int n=0,m=0;
printf("&n=%p\n",&n);
printf("&m=%p\n",&m);
int* p = NULL;//p指向NULL,但是由于p是指针变量,因此是有地址的。
int** pp = &p;
scanf("%p",pp);
scanf("%d",p);
printf("n=%d\nm=%d\n",n,m);
}
image.png
一维指针存放变量地址,二维指针存放一维指针地址。
- 示例二:
// 指针与数组之间关系
int arr[6] ={1,2,3,4,5,6};
int* p = arr;
for(int i=0;i<6;++i){
printf("%d\n",p[i]);
}
// 二维指针与指针数组之间关系
int arr2[6] ={7,8,9,10,11,12};
int* parr[] = {arr,arr2};
int** pp = parr;
for(int i=0;i<2;++i){
for(int j=0;j<6;++j){
printf("%d ",pp[i][j]);
}
printf("\n");
}
一维指针存放数组地址,二维指针存放指针数组地址。
(12)二维指针的使用
<1>二维指针作为函数参数
- 传入一维指针地址
传入一维指针地址可以取出函数内部申请的动态内存和单个变量。也可以取出一个数组
void Func(int** pp){
int* p = (int*)malloc(sizeof(int));
printf("*pp=%p\t&p=%p\tp=%p\t*p=%d\n",*pp,&p,p,*p);
*p=100;
*pp=p;
printf("*pp=%p\t&p=%p\tp=%p\t*p=%d\n",*pp,&p,p,*p);
}
int main(){
int* p =NULL;
printf("&p=%p\tp=%p\n",&p,p);//0地址是不能访问的地址
Func(&p);
printf("&p=%p\tp=%p\t*p=%d\n",&p,p,*p);
free(p);
p=NULL;
}
- 传入指针数组地址
void PrintStrings(const char** strs,int n){
for(int i=0;i<n;++i){
printf("%s\n",strs[i]);
}
}
int main(){
const char* strs[] = {
"abcd",
"1234",
"甲乙丙丁"
};
PrintStrings(strs,3);
}
<2>二维指针作为函数返回值
二维指针通常用作指针数组的返回值类型。
- 创建m*n的单位矩阵
int** CreateIdentityMatrix(int r,int c){
int** pm = (int**)malloc(sizeof(int*)*r);
for(int i=0;i<r;++i){
pm[i] = (int*)malloc(sizeof(int)*c);
for(int j=0;j<c;++j){
pm[i][j] = (i==j);
}
}
return pm;
}
void PrintMatrix(int** pm,int r,int c){
for(int i=0;i<r;++i){
for(int j=0;j<c;++j){
printf("%d ",pm[i][j]);
}
printf("\n");
}
}
void DestoryMatrix(int** pm,int r,int c){
for(int i=0;i<r;++i){
free(pm[i]);
}
free(pm);
pm = NULL;
}
int main(){
int r,c;
scanf("%d%d",&r,&c);
int** pm = CreateIdentityMatrix(r,c);
PrintMatrix(pm,r,c);
DestoryMatrix(pm,r,c);
<3>练习
- 实现函数输入正数n,返回三角星号图像字符串数组
char** CreateStar(int n){
char** star=(char**)malloc(n*sizeof(char*));
for(int i=0;i<n;++i){
star[i]=(char*)malloc((i+1)*sizeof(char));
memset(star[i],'*',i+1);
}
return star;
}
- 打印从n-m中随机行随机列的*字符串数组。
char** createStars(int n,int m,int* r,int** c){//需要返回行,每行有多少列,及二维矩阵
srand(time(NULL));
int row=rand()%(m-n+1)+n;//生成从n-m的随机数(包括n和m)
char** ps=(char**)malloc(sizeof(char*)*row);
int* cs=(int*)malloc(sizeof(int)*row);//由于每行中的个数是随机的,所以需要数组来记录每行的数量。
for(int i=0;i<row;++i){
int col=rand()%(m-n+1)+n;
cs[i]=col;
ps[i]=(char*)malloc(sizeof(char)*col);
memset(ps[i],'*',col);//给二位矩阵赋值
}
*r=row;
*c=cs;
return ps;
}
void printStars(char** stars,int r,int* c){
for(int i=0;i<r;++i){
for(int j=0;j<c[i];++j){
printf("%c",stars[i][j]);
}
printf("\n");
}
}
void destroy(char** stars,int r,int* c){
for(int i=0;i<r;++i){
free(stars[i]);
}
free(stars);
*stars=NULL;
free(c);
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
int r;
int* c;
char** stars=createStars(n,m,&r,&c);//要从子函数中返回行和列。
//每列的大小都是随机值,所以需要一个数组。
printStars(stars,r,c);
destroy(stars,r,c);
}
3.数组
(1)定义
数组是存储一个固定大小的相同类型元素的顺序集合。
(2)数组的使用
<1>定义数组
类型 数组名[元素个数];
<2>初始化数组
类型 数组名[元素个数] = {值1,值2,值3,值4,等等};
-
简化
初始化数组可以不指定数组大小,此时数组的大小则为初始化时元素的个数。int arr[]= {1,2,3,4,5};
<3>访问数组元素
数组元素可以通过数组名称加索引(下标)进行访问。注意:数组的索引(下标)是从0开始。
数组名[索引]
每个数组元素都是一个变量,变量的类型就是数组声明时的类型
<4>数组遍历
for (int i=0;i<n;i++){ // 依次生成从0~n-1个数组索引
arr[i] // 访问数组的每一个元素
}
(3)特点
- 数组创建后大小不能修改。
- 数组内所有元素具有相同数据类型。
- 数组中元素在内存中是依次连续排列的。
(4)初始化
<1>整体初始化
- 数组未初始化,数组里面的值都是随机值。int arr[10];
- 数组初始化为{0},数组里面的值都是0。int arr[10]={0};
- 数组初始化为{非零值},数组里面第一个值是非零值,其他的值都是0。int arr[10]={2};
<2>部分初始化
int arr[12] = {[2]=2,[5]=5};//下标2和5赋值
指定的下标被赋值,其他的值都是0。这是C99语法。
<3>大小
sizeof给出整个数组所占据的内容的大小(单位是字节)。数组大小=元素大小*数组个数
。
<4>赋值
- 以下赋值是错误的。
int arr[5]={1,2,3,4,5};
int b=arr;//指针不能直接赋值给变量。左边是int类型的变量,右边是指针。
- 赋值方式
方式1:
int* p =malloc(sizeof(int)*n);
p=arr;
方式2:memcpy
int b[5];
memcpy(b,arr,sizeof(arr));
方式3:循环进行挨个复制
方式4:(只针对字符数组)
int b[12];
strcpy(b,arr);
(5)数组与指针
数组名是数组第一个元素的地址。数组下标实现的操作指针也可以实现。
- 一维数组指针用法
- 二维数组指针用法
- 结论:
(1)二维数组数组名是第一个元素的首地址;
(2)在二维数组中a[i]就是一个一维数组。
int days[4][3]={31,28,31,30,31,30,31,31,30,31,30,31};
printf("days=%p\n",days);
printf("days[0]\t\t = %p\n&days[0][0]\t = %p\n",days[0],&days[0][0]);
printf("days[1]\t\t = %p\n&days[1][0]\t = %p\n",days[1],&days[1][0]);
printf("days[2]\t\t = %p\n&days[2][0]\t = %p\n",days[2],&days[2][0]);
printf("days[3]\t\t = %p\n&days[3][0]\t = %p\n",days[3],&days[3][0]);
(6)函数与数组
<1>数组作为函数参数
数组作为函数参数时,通常必须再用一个参数传入数组大小。
-
形式1:
返回值类型 函数名(类型 参数名[],int size){}
-
形式2:
返回值类型 函数名(类型* 参数名,int size){}
数组作为参数时,数组退化成指针,不能利用sizeof获取数组大小,也就不能计算数组元素个数,所以需要传入数组的大小。
<2>从函数返回数组
C 语言不允许返回一个完整的数组作为函数的参数。通过指定不带索引的数组名来返回一个指向数组的指针。
类型* 函数名() {
return 数组名;
}
示例:返回一个指向随机数组的指针
int* createRandArray(int n) {
int* arr =(int*)malloc(n*sizeof(int));
srand(time(NULL));
for(int i=0; i<n; ++i) {
arr[i] = rand()%100+1;
}
return arr;
}
(7)多维数组
<1>声明
类型 数组名[元素个数1][元素个数2]...[元素个数N];
多维数组最常用形式是二维数组。二维数组相当于一个行列组成的表。
类型 二维数组名[行数][列数];
int days[4][3];
<2>初始化二维数组
多维数组可以通过在括号内为每行指定值来进行初始化。
int days[4][3]={
{31,28,31},
{30,31,30},
{31,31,30},
{31,30,31}
};
- 简化:
(1)省略内部嵌套括号。
int days[4][3]={31,28,31,30,31,30,31,31,30,31,30,31};
(2)省略第一维大小,第二维不能省略。
int days[][3]={31,28,31,30,31,30,31,31,30,31,30,31};
<3>访问二维数组元素
二维数组中的元素是通过使用下标(即数组的行索引和列索引)来访问的。
days[0][1] = 29;
<4>二维数组元素遍历
通常使用嵌套循环来处理二维数组。
for (int i = 0; i < 4; i++ ) {
for (int j = 0; j < 3; j++ ) {
printf("a[%d][%d] = %d\n",i,j,a[i][j]);
}
}
<4>多维数组
大于二维的数组的用法与二维数组一样,只是使用比较少。
int days[][4][3]={
{31,28,31,30,31,30,31,31,30,31,30,31}, // 平年
{31,29,31,30,31,30,31,31,30,31,30,31} // 闰年
};
printf("平年二月天数为%d\n",days[0][0][1]);// 平年第一季度第二个月
printf("闰年二月天数为%d\n",days[1][0][1]);// 闰年第一季度第二个月
多维数组初始化只能第一个维度可以省略。
(8)const数组
<1>const数组是什么?
const int arr[]={1,2,3,4,5,};
数组变量已经是const指针,表示数组中的每一个元素都是const int,即每个元素不能通过arr改变。
arr[0]=0;//错误
<2>const数组怎么用?
保护数组值
因为数组作为函数参数是以地址方式传递的,所以函数内部可以修改数组的值。
为了保护数组不被函数破坏,可设置参数为const。
int sum(const int* arr,int len);
(9)一维数组的多维使用方式
<1>一维数组转二维数组
一维数组使用二维数组访问方式(二重循环),可看作盒子按照行列方式摆放。
元素下标 = 当前行号*列的个数+当前列号
for(int i=0;i<rows;++i){
for(int j=0;j<cols;++j){
printf("%d ",arr[i*cols+j]);
}
printf("\n");
}
<2>一维数组转三维数组
元素下标=当前面序号面的个数+当前行号列的个数+当前列号
4.字符串
(1)字符串是什么?
存放字符(char)的数组称为字符数组。在C语言中,使用NULL字符('\0')终止的一维字符数组被称作字符串。
字符串的变量名代表该数组的首地址。
(2)字符串怎么用?
<1>声明
字符串初始化方式与普通数组一样。
char 字符串变量名[字符数量];
<2>初始化
字符串可以按照普通数组初始化方式初始化。
char greeting[12] = {'H', 'e', 'l', 'l', 'o',' ','W','o','r','l','d','\0'};
注意字符串最后一个字符必须是\0。
-
简化:(主要使用这个)
char 字符串变量名[字符数量] = 字符串字面量; char greeting[] = "Hello World";
<3> 输入输出
字符串输入输出与数值是一样的,使用函数scanf()和printf()。只是占位符不同,字符串格式占位符为%s。
char str[1024];
scanf("%s",str);
printf("你好,%s\n",str);
scanf()
读入一个单词直到空白符(空格、回车、Tab),scanf()
不安全,因为不知道要读入的内容长度,容易溢出。
- 解决方式:指定读取的长度。
char str[8];
scanf("%7s",str);
printf("%s\n",str);
%与s之间的数字表示最多允许输入的字符数,这个数字要比数组长度少1,留一个是给‘\0’。
<4>访问字符
字符串的访问方式与数组数字是一样的,按照索引/下标方式访问。
<5>遍历
- 方式1:
for (int i=0; '\0' != str[i]; ++i){ // 依次生成字符数组索引
str[i] // 访问数组的每一个字符
}
- 方式2:
char* str="hello world";
while('\0' != *str){
printf("%c\n",*str++);
}
<6>赋值
char* s = "Hello World";
char* t;
t = s;
printf("%s\n",t);
没有产生新的字符串,只是s和t指向相同的字符串。s和t的地址相同。
(3) 注意
- 0就是数字0,'\0'就是转义的数字0,本质上与数字0一样,'0' 就是字符0,类似于字符'1'等等。
- 有些时候,字符串声明时没有初始化,这时字符串里的值是随机值。需要手动赋值。整体初始化:char buf[100] = { 0 };
- strlen: 测字符串长度,不包含数字0,字符'\0' ;sizeof:测数组长度,包含数字0,字符'\0'。
char buf2[50] = { '1', 'a', 'b', 0, '7' };
printf("buf2 = %s\n", buf2);//1ab
printf("strlen:%1d\tsizeof:%d\n",strlen(buf2),sizeof(buf2));//3 50
char buf3[50] = { '1', 'a', 'b', '\0', '7' };
printf("buf3 = %s\n", buf3);
printf("strlen:%1d\tsizeof:%d\n",strlen(buf3),sizeof(buf3));//3 50
(4)c中字符串的表示方式(*)
C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量。
<1>字符数组
字符数组存放在全局数据区或栈区,使得字符数组可以读取和修改。
char str[]="hello";//数组形字符串存放在全局数据区或栈区,可读可写
char* p=str
printf("%s\n",p);
<2>字符串指针(字符串常量)
- 字符串常量存放在常量区,因此只能读取不能修改,任何对它的赋值都是错误的。
- str是一个指针,初始化指向一个字符串常量。(在C99标准中,str1报警告,提示应该使用const char*),修改字符串常量可能会导致严重后果。
char* str="hello";//指针字符串存放在常量区,只读不能写。
printf("%s\n",str);//直接通过str打印该字符串,不用解引用
str="world";//正确的。相当于const int* p.可以修改指针的指向,不能修改其内容。
str[0]='H';//错误
<3>选择
在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。
(5)字符数组和字符串指针的区别
<1>sizeof
与strlen()
char arr[] = "Hello World";
char* ptr = "Hello World";
printf("sizeof(arr) = %ld\n",sizeof(arr));//12(包含'\0')
printf("strlen(arr) = %ld\n",strlen(arr));//11(不包含'\0')
printf("sizeof(ptr) = %ld\n",sizeof(ptr));//8(指针的字节都是8)
printf("strlen(ptr) = %ld\n",strlen(ptr));//11
<2>替换字符
- 修改字符数组(可以)
char arr[] = "Hello World";
arr[0] = 'h';
- 字符串指针(吐核)
char* ptr = "Hello World";;
*ptr = 'h';
*(ptr+6) = 'w';
- 指向字符数组的字符串指针(可以)
char arr[] = "Hello World";
char* ptr = arr;
*ptr = 'h';
*(ptr+6) = 'w';
- const字符数组(不能)
const char arr[] = "Hello World";
arr[0] = 'h';
- 指向const字符数组的字符串指针(可以,会报警告)
const char arr[] = "Hello World";
char* ptr = arr;
*ptr = 'h';
决定能否修改的是指针指向的值能否修改。const的限制只针对定义为const的变量。
(6)字符串数组与字符串指针数组
<1> 字符串数组
字符串数组,可以看成二维字符数组,只是初始化可以使用字符串方式。
char arr1[12][10] = {"January","February","March","April","May","June","July",
"August","September","October","November","December"};
- 简化
char arr1[][10] = {"January","February","March","April","May","June","July",
"August","September","October","November","December"};
<2>字符串指针数组
char* arr2[12] = {"January","February","March","April","May","June","July",
"August", "September","October","November","December"};
- 或者:
char* arr2[] = {"January","February","March","April","May","June","July",
"August", "September","October","November","December"};
<3>区别
- 大小
printf("sizeof(arr1)=%d\n",sizeof(arr1));//120=12*10;
printf("sizeof(arr2)=%d\n",sizeof(arr2));//96=12*8
- 二维指针的区别:
char** p1 = arr1;//会报警告(不兼容的指针类型:incompatible pointer type)
for(int i=0;i<12;++i){
printf("%s\n",p1[i]);//执行会吐核,只能使用arr1[i]来输出。
}
char** p2 = arr2;//正确
for(int i=0;i<12;++i){
printf("%s\n",p2[i]);
(7)字符串与函数
<1> 字符串传参
字符串传参方式与数组传参方式一样,只不过很多时候不需要传递字符串的长度。
因为在函数里面可以使用strlen函数来求字符串长度。
void print_string(char str[]){}
或者:
void print_string(char* str){}
<2>字符串返回
字符串返回只能使用指针char*。
(8)字符串函数
<1>字符串长度
size_t strlen(const char *s);
返回字符串长度不包含\0。
<2>字符串比较
int strcmp(const char *s1,const char *s2);
- 返回值
返回0,表示s1 == s2
返回>0,表示s1 > s2
返回<0,表示s1 < s2
<3>字符串拷贝
char* strcpy(char* restrict dst,const char* restrict src);
- 注意:
(1)由于dst参数将进行修改,所以它必须是个字符数组或者是一个指向动态分配内存数组的指针,不能使用字符串常量。
(2)必须保证目标字符数组的空间足以容纳需要复制的字符串。
<4>字符串连接
char* strcat(char* restrict s1,const char*restrict s2);
把s2拷贝到s1的后面,拼接成一个长的字符串。返回s1,注意:s1必须有足够的空间。
strcpy和strcat都会有安全问题:dst空间不足,出现越界。
<5>字符查找
char* strchr(const char*s,int c);//从左往右
char* strrchr(const char*s,int c);//从右往左
注意:第2个参数是一个整型值,其实表示一个字符值。返回找到字符的指针,没找到返回NULL.
<6>子串查找
char* strstr(const char*s1,const char*s2);
char* strcasestr(const char*s1,const char*s2);
功能:在s1中查找整个s2第1次出现的起始位置,并返回一个指向该位置的指针。如果s2并没有完整地出现在s1的任何地方,函数将返回一个NULL指针。如果第2个参数是一个空字符串,函数就返回s1。
<7>文档
char *date="20191221";
//sscanf()
int year,month,day;
sscanf(date,"%4d%2d%2d",&year,&month,&day);//读出数据
printf("Y:%d\nM:%d\nD:%d\n",year,month,day);
//sprintf()
//格式:yyyy年mm月dd日
char str[18]; //8(日期)+3*3(一个汉字3个字节)+1(\0)
sprintf(str,"%d年%d月%d日",year,month,day);//写入数据
printf("%s\n",str);
<8>scanf()和gets()的区别:
- 相同点
都可用于输入字符串 - 不同点
(1)gets函数总结:
1.只能读取字符串
2.gets() 从标准输入设备读取字符串,以回车结束读取,使用'\0'结尾,回车符'\n'被舍弃没有遗留在缓冲区。
3.可以用来输入带空格的字符串
4.可以无限读取,不会判断上限,因此使用gets不安全,推荐使用fgets,可能会造成溢出
char string [256];
gets (string); // warning: unsafe (see fgets instead)
printf(%s\n,string);
(2)scanf()函数总结:
1.可以读取任何类型的数据
2.scanf() 以 空格 或 回车符 结束读取,空格 或 回车符 会遗留在缓冲区。
3.不能直接输入带空格的字符串
4.scanf( )函数输入代空格的字符串:scanf("%[^\n]", a);//%[]输入字符集, [^\n] 表示除了'\n'之外的字符都接收,即可以接收空格,这个可以用来输入带空格的字符串
<9>printf和puts的区别
-
puts()函数只用来输出字符串,没有格式控制,里面的参数可以直接是字符串或者是存放字符串的字符数组名。
puts(string);
-
printf()函数的输出格式很多,可以根据不同格式加转义字符,达到格式化输出。
puts()函数的作用与语句printf("%s\n",s);的作用形同
<10>字符串常量连接
两个相邻字符串常量会自动连接。
char* greeting = "Hello" "World";//HelloWorld
<11>判断字符和数字的函数
头文件:#include<ctype.h>
-
isalnum
函数
功能:判断字符变量c是否为字母或数字
说明:当c为数字0-9或字母a-z及A-Z时,返回非零值,否则返回零 -
isupper
函数
功能:判断字符c是否为大写英文字母
说明:当参数c为大写英文字母(A-Z)时,返回非零值,否则返回零。 -
islower
函数
功能:判断是否是小写字母 -
isdigit
函数
函数说明:该函数主要是识别参数是否为阿拉伯数字0~9。
返回值:若参数c为数字,则返回TRUE,否则返回NULL(0)。 -
isalpha
函数
功能:判断是否是字符
5.结构体
(1)结构体定义
结构体是不同类型的多个变量绑到一起。
(2)结构体怎么用
<1>定义结构体
结构体里面的成员定义方式与变量相同,也就是在结构体里面定义了多个变量。
struct 结构体名{
成员列表;
};
<2>定义结构体变量
struct Student student1;
Student类型的结构体变量student1,这个变量就可以代表一个学生,他拥有姓名、年龄、成绩这三个成员。
<3>结构体成员引用
结构体不能进行整体的输入和输出,需要对成员分别操作,这称为结构体变量成员引用。
结构体变量名.成员名
scanf("%s%d%f",&student1.name,&student1.age,&student1.score);
<4>结构体成员赋值
结构体变量成员赋值就是给结构体内所有成员依次赋值。
strcpy(student1.name,"张三");
student1.age = 19;
student1.score = 90.5;
注意:数值类型成员可以直接赋值,字符串类型变量需要使用字符串复制函数
<5>结构体整体赋值
结构体变量整体赋值就是给结构体所有成员一起赋值。
struct Student student2;
student2 = student1;
结构体能整体赋值,数组不能直接赋值。
<6>结构体整体初始化(常用)
struct Student student2 = {"李四",18,95.5};
注意:赋值数据顺序必须与结构体成员声明顺序一致。
<7>结构体部分初始化
struct Point3D{
int x;
int y;
int z;
};
struct Point3D p = {.x=10,.z=20};
未初始化的成员的值是随机值。
(3)其他语法
<1>定义结构体并同时定义结构体变量
struct Student{
char name[32]; //姓名
int age; //年龄
float score; //成绩
} student1,student2;
<2>定义结构体并同时定义结构体变量并赋初值
struct Student{
char name[32]; //姓名
int age; //年龄
float score; //成绩
} student1 = {“Zhang”, 19, 90.5};
(4)结构体操作
<1>取地址
结构体名不是结构体变量的地址,必须使用&获取地址。
struct Point3D{
int x;
int y;
int z;
};
struct Point3D p = {1,2,3};
printf("&p = %p\n",&p);
printf("&(p.x) = %p\n",&p.x);
printf("&(p.y) = %p\n",&p.y);
printf("&(p.z) = %p\n",&p.z);
<2>传参
- 整个结构体作为参数的值传入函数。这时候在函数内新建一个结构体变量并赋值。
- 结构体可以作为返回值,也是结构体整体赋值。
void Print(struct Point3D p){
printf("(%d,%d,%d)",p.x,p.y,p.z);
}
(5)结构体指针
struct Point3D p = {1,2,3};
struct Point3D* q = &p;
<1>结构体指针访问成员
-
结构体变量使用
.
和名字访问成员。 -
结构体指针使用
->
和名字访问成员。
格式:结构体指针->成员名 printf("(%d,%d,%d)",q->x,q->y,q->z); // 等同于printf("(%d,%d,%d)",(*q).x,(*q).y,(*q).z);
通过修改结构体指针q指向的成员,也会改变结构体变量p成员的值。
<2>结构体指针作为参数
在C语言中,通常会将结构体指针作为参数传入函数,尤其是当传递的参数类型比地址大的时候,可以使用这种方式既能传递较少的字节数。
(6)结构体数组
struct Point3D ps[] = {{1,2,3},{1,1,1},{0,0,0}};
for(int i=0;i<3;++i){
printf("(%d,%d,%d)\n",ps[i].x,ps[i].y,ps[i].z);
}
(7)结构体嵌套
struct Line{
struct Point3D start;
struct Point3D end;
};
struct Line line = {{1,1,1},{0,0,0}};
// 使用
printf("(%d,%d,%d)~(%d,%d,%d)",line.start.x,line.start.y,line.start.z,line.end.x,line.end.y,line.end.z);
//结构体指针嵌套结构体
struct Line* p = &line;
printf("(%d,%d,%d)~(%d,%d,%d)",p->start.x,p->start.y,p->start.z,p->end.x,p->end.y,p->end.z);
- 结构体含有结构体数组
struct Triangle{
struct Point3D p[3];
};
struct Triangle t = {{{1,2,3},{1,1,1},{0,0,0}}};//里面两层是结构体数组
(8)使用结构体
在C语言标准库time.h
中,有一个tm
结构体用来获取时间。
-
time()
:获得的日历时间time_t
,从公元1970年1月1日0时0分0 秒算起至今的UTC时间所经过的秒数。 -
gmtime()
:将日历时间time_t
转化为UTC时间(世界标准时间,即格林尼治时间)tm
结构体。 -
localtime()
:将日历时间time_t
转化为本地(当前时区)时间tm
结构体。 - 获取当地时间
#include <stdio.h>
#include <time.h>
int main(){
time_t now = time(NULL); // 获取当前时间
// 获取UTC时间
struct tm* utc = gmtime(&now);
printf("UTC:%d-%d-%d %d:%d:%d\n",utc->tm_year+1900,utc->tm_mon+1,utc->tm_mday,utc->tm_hour,utc->tm_min,utc->tm_sec);
// 获取本地时间
struct tm* local = localtime(&now);
printf("Local:%d-%d-%d %d:%d:%d\n",local->tm_year+1900,local->tm_mon+1,local->tm_mday,local->tm_hour,local->tm_min,local->tm_sec);
}
<9>结构体的sizeof
- C语言中的空struct的sizeof为0。
- 对象的大小不包含静态成员变量
- 以最大的为基准,补齐排列。
6.联合体
(1)定义
用法与struct
一样。不同点是所有成员公用相同的内存空间。联合体的sizeof是成员中大小最大的值。
union 联合体类型名 {
成员
};
- 代码
共用内存,修改一个成员的值,其他成员也受到影响。
union data{
int n;
char ch;
short m;
};
int main(){
union data a;
printf("%d, %d\n", sizeof(a), sizeof(union data) );//4,4
a.n = 0x40;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);//40,@,40
a.ch = '9';
printf("%X, %c, %hX\n", a.n, a.ch, a.m);//39,9,39
a.m = 0x2059;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);//2059,Y(只有一个字节,8位二进制,2位16进制,所以只有59,对应ascii位Y),2059
a.n = 0x3E25AD54;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);//3E25AD54,T,AD54(两个字节)
}
联合体data的内部结构。
image.png
联合体是成员共用内存空间。
(2)注意
- 联合体虽然可以有多个成员,但同一时间只能存放其中一种;
- 对于联合体来讲最基本的原则是,一次只操作一个成员变量,如果这个变量是指针,那么一定是处理完指针对应的内存之后再来使用其他成员。所以在联合体中,基本不会使用指针。
7.枚举
(1)枚举
<1>定义
枚举是一种用户定义的数据类型。
<2>枚举怎么用
枚举大括号里面的名字是常量符号,类型为int,值依次从0到n。
enum 枚举类型名{名字0,名字1,名字2,...,名字n};
枚举就是给这些常量值,规定一个名字。
enum Week{Sun,Mon,Tues,Wed,Thur,Fri,Sat};
(1)枚举量可以直接作为值使用。
(2)枚举类型可以直接作为类型使用。
enum Mouth{Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sept,Oct,Nov,Dec};
int month_days[]={31,28,31,30,31,30,31,31,30,31,30,31};
char* month_names[]={"一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"};
void PrintMonth(enum Mouth m){//枚举类型可以直接作为类型使用
printf("%s月有%d天\n",month_names[m],month_days[m]);
}
void main(){
enum Mouth m = Jan;//枚举量可以直接作为值使用。
//printf("%d\n",m);//m=0;
PrintMonth(m);
}
<3>指定枚举量的值
// 常用进制
enum Radix{Bin=2,Oct=8,Dec=10,Hex=16};
或者:
指定其中一个值,后面的值依次加1。
enum Mouth{Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sept,Oct,Nov,Dec};
(2)常量符号化
程序中的数字有时含义不明,被称为魔术数字。通常使用符号来表示。
<1>const
const double PI = 3.1415926;
// 星期
const int SUM = 0;
const int MON = 1;
<2>#define
#define PI 3.1415926
// 星期
#define SUM 0
<3>枚举
(3)const
与#define
区别
No. | 比较项 | #define |
const |
---|---|---|---|
1 | 编译处理 | 预处理阶段 | 编译、运行阶段 |
2 | 工作原理 | 简单的字符串替换 | 有对应的数据类型 |
3 | 存储方式 | 展开,在内存中有若干个备份 | 只读变量在内存中只有一份 |
4 | 类型检查 | 没有类型安全检查 | 在编译阶段进行类型检查 |
5 | 作用域 | 从定义开始,任何位置都可访问 | 只能在变量作用域内 |
- 作用域:
void func (){
#define N 12
const int n = 12;
}
void main(){
printf("%d\n",N);//定义以后任何位置都可以访问
printf("%d\n",n);//不能访问!!!(只能在作用域内访问)
}
8.类型重命名typedef
(1)类型重命名是什么?
给一个已有的数据类型声明一个新名字。新名字是数据类型的别名。
(2)类型重命名怎么用?
<1> 基本类型重命名
类型重命名用法与变量定义相似,只是在前面加上typedef
。
-
语法:
typedef 类型 新名字;
-
实例:
typedef char* Str;
Str str = "ABCDEFG";
<2>结构体/联合体类型重命名
我们使用结构体类型时,需要使用struct
关键字。typedef
可以省略这个关键字。
- 语法
typedef struct {
成员;
} 类型名;
- 实例
typedef struct Point3D{
int x;
int y;
int z;
} Point3D_t;
Point3D_t p = {1,2,3};//等价于struct Point3D p={1,2,3};
- 有时结构体的类型名可以省略:
typedef struct{
int x;
int y;
int z;
} Point3D;
- 在typedef定义结构体同时,可以定义结构体指针
typedef struct{
int x;
int y;
int z;
} Point3D,*pPoint3D;
Point3D p = {1,2,3};
pPoint3D q = &p;
<3>函数指针类型重命名
-
语法
typedef 返回类型 (* 函数指针类型)(参数)
-
实例:
int add(int a,int b){return a+b;}
typedef int (*opt)(int,int); // 定义函数指针类型
opt fpadd = &add; // 定义函数指针并赋值
printf("%d\n",(*fpadd)(1,3));
(3)类型重命名有什么用
- 为现有类型创建别名,定义易于记忆的类型名。
- 简化代码。
- 便于批量修改具体类型。
(4)小结
9.动态分配内存
(1)动态分配内存是什么?
是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。通常我们的变量都是预先分配的,系统自动给分配和回收的。
(2)动态分配内存怎么用?
C99可以使用变量作为数组定义的大小,在C99之前只能使用动态分配内存实现。
int arr[n];
近似于:
int* arr=(int*)malloc(sizeof(int));// 操作arr如同操作int arr[n]
free(arr);
<1>申请动态分配内存malloc()
-
头文件
<stdlib.h>
-
函数原型
void* malloc(size_t size),
-
功能
向系统申请大小为size
字节的内存空间。 -
返回值
返回结果是void*
,使用时转换成需要的指针类型。如果申请失败,返回NULL
。
<2> 释放动态分配内存free()
free()
归还申请的内存。
- 有关
free
的坑:
(1)忘记free
(造成内存泄漏)
(2)修改了申请地址,然后free()
。(free(): invalid pointer;Aborted (core dumped))
int* arr = (int*)malloc(n*sizeof(int));
free(arr+1);
(3)多次free()
。(编译不会出错,但是在valgrind时会出错,多次释放)
(4)free()
非malloc
内存。(free(): invalid pointer;Aborted (core dumped))
<3>初始化动态分配内存
malloc申请内存,内部初始值是随机值,没有经过初始化。(gcc是0,但vs中是随机值)
- 使用malloc什么空间的初始化:
int* arr = (int*)malloc(n*sizeof(int));
memset(arr,0,n*sizeof(int));
free(arr);
-
calloc
calloc申请内存,内部初始值是0;
int* arr = (int*)calloc(n,sizeof(int));
free(arr);
近似于:
int arr[n]={0};
<4>重新调整内存的大小
void* realloc (void* ptr, size_t size);
- ptr -- 指针指向一个要重新分配内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果为空指针,则会分配一个新的内存块,且函数返回一个指向它的指针。
- 说明:
(1)如果当前内存段后面有足够的内存空间,那么就直接扩展这段内存,realloc()返回原来的首地址;
(2)如果当前内存段后面没有足够的内存空间,那么系统会重新向内存树申请一段合适的空间,并将原来空间里的数据块释放掉,而且realloc()会返回重新申请的堆空间的首地址;
(3)如果创建失败,返回NULL, 此时原来的指针依然有效;
<5>应用
从终端输入未知数量的数字,按键Ctrl+D作为结束,逆序输出输入的数字。
- 使用realloc
int main(){
int n;
int count=0;
int* arr=NULL;
while(scanf("%d",&n)!=EOF){
++count;
arr=(int*)realloc(arr,count*sizeof(int));
arr[count-1]=n;
}
for(int i=0;i<count;++i){
printf("%d ",arr[count-1-i]);
}
free(arr);
arr=NULL;
}
- 使用malloc
while(scanf("%d",&n)!=EOF){
int* t=(int*)malloc((count+1)*sizeof(int));
if(NULL!=arr){
memcpy(t,arr,sizeof(int)*count);
t[count]=n;
free(arr);
arr=t;
}else{
arr=t;
arr[count]=n;
}
++count;
}
(3)有关内存操作的函数
头文件<string.h>
<1>memset
void *memset(void *s, int c, unsigned long n);
作用是在一段内存块中填充某个给定的值。可以为任何类型的数据进行初始化。
<2>memcpy
void *memcpy(void *dest, const void *src, size_t n);
src的开始位置拷贝n个字节的数据到dest。如果dest存在数据,将会被覆盖。memcpy函数的返回值是dest的指针。size_t n:其实就是用sizeof来获取的。
(4)野指针和悬空指针
<1>定义
- 野指针:访问一个已销毁或者访问受限的内存区域的指针,野指针不能判断是否为NULL来避免
- 悬空指针:指针正常初始化,曾指向一个对象,该对象被销毁了,但是指针未置空,那么就成了悬空指针。
<2>野指针产生的原因
(1)指针定义时未被初始化;
(2)指针被释放时没有置空;
(3)指针操作超越变量作用域.
<3>规避方法
(1).初始化指针的时候将其置为nullptr,之后对其操作。
(2).释放指针的时候将其置为nullptr。
网友评论