c语言基础知识梳理,供大家参考,查阅。
指针数组
- 定义:
本质是数组,里面存放的是指针
- 格式:
存储类型 数据类型 *数组名[元素个数]
int *arr[3] = {};
- 应用实例:
3.1. 用于存放普通变量的地址
int a = 10, b = 20, c = 30;
int *arr[3] = {&a, &b, &c};
访问b的地址:
arr[1] *(arr+1)
访问 b 的值
*(arr[1]) *(*(arr+1))
3.2. 用于存放二维数组的每一行第一个元素的地址(列地址)
int a[2][3] = {1, 2, 3, 4, 5, 6};
int *arr[2] = {a[0], a[1]};
访问 a[1][2] 的地址:
arr[1]+2 *(arr+1)+2
arr:第一个元素的地址
arr+1:第二个元素的地址
*(arr+1):第二个元素:a[1]:第二行第一列的地址
*(arr+1)+2:第二行第三列的地址 &a[1][2]
3.3. 用于存放字符串
char str[32] = "hello"; // 存放的是整个hello字符串
char *str = "hello"; // 存放的是hello字符串的首地址
char str[32] = "hello";
char *strs = "hello";
printf("%s\n", str);
printf("%s\n", strs);
printf("%p %p\n", strs, &str[0]);
printf("%c %c\n", *(strs+1), str[1]);
使用指针数组存放字符串
char *str[3] = {"hello", "world", "xiaomisu7"};
打印 "xiaomisu7" 字符串
char *str[3] = {"hello", "world", "xiaomisu7"};
printf("%s\n", str[2]); // xiaomisu7
printf("%s\n", *(str+2)); // xiaomisu7
printf("%s\n", *(str+2)+4); // misu7
打印 'm' 这个字符
printf("%c\n", *(*(str+2)+4)); // m
printf("%c\n", *(str[2]+4)); // m
练习:用指针将整型组s[8]={1,2,3,4,5,6,7,8}中的值逆序存放。
练习:用变量a给出下面的定义(3C科技、宇视科技,神思电子,中安云科,北京凝思软件)
a) 一个整型数:· int a;
b) 一个指向整型数的指针: int *a;
c) 一个指向指针的的指针,它指向的指针是指向一个整型数: int **a;
d) 一个有10个整型数的数组: int a[10];
e) 一个有 10个指针的数组,该指针是指向一个整型数的 int *a[10];
f) 一个指向有 10个整型数数组的指针: int (*a)[10];
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数:
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
函数
- 定义
一个完成特定功能的代码模块
- 三要素
功能、参数、返回值
- 格式
存储类型 数据类型 函数名(参数列表)
{
函数体;
}
- 没有参数:参数列表可以省略,也可以用void
- 没有返回值:数据类型为 void , 函数内部没有return语句。
- 有返回值:要根据返回值的数据类型定义函数的数据类型
- 定义子函数时直接定义在主函数上面,如果定义在主函数下面需要提前声明函数
- 函数声明
数据类型 函数名(参数列表); // 形参
- 函数调用
- 没有返回值:直接调用:函数名(实参);
- 有返回值:如果需要接收返回值,就要定义一个与返回值类型相同的变量去接收
如果不需要接收返回值,就直接调用函数
#include <stdio.h>
void fun()
{
printf("hello\n");
}
void add1(int a, int b)
{
printf("sum = %d\n", a + b);
}
int add2(int a, int b)
{
return a + b;
}
int sub(int a, int b);
int main(int argc, char const *argv[])
{
fun();
add1(1, 2);
int sum = add2(3, 5);
printf("%d\n", sum);
int su7 = sub(7, 3);
printf("%d\n", su7);
return 0;
}
int sub(int a, int b)
{
return a - b;
}
练习1:编写一个函数,函数的2个参数,第一个是一个字符,第二个是一个char *,返回字符串中该字符的个数。
#include <stdio.h>
int count(char c, char *s)
{
int n = 0;
while (*s != '\0')
{
if(c == *s)
{
n++;
}
s++;
}
return n;
}
int main(int argc, char const *argv[])
{
printf("请输入一个需要查找的字符:");
char c = getchar();
char str[32];
printf("请输入一个字符串:");
scanf("%s", str);
int num = count(c, str);
printf("查找到该字符个数为:%d个\n", num);
return 0;
}
练习2:编程实现strlen函数的功能,strlen计算字符串实际长度,不包含’\0’
- 函数传参
- 值传递
单向传递,将实参传递给形参使用,改变形参实参不会受到影响
#include <stdio.h>
int fun(int num, int sum)
{
num++;
sum++;
return num + sum;
}
int main(int argc, char const *argv[])
{
int a = 3, b = 4;
int ret = fun(a, b);
printf("%d %d %d\n", a, b, ret); // 3 4 9
return 0;
}
-
- 地址传递
双向传递,在函数中修改形参,实参也会随之变化
#include <stdio.h>
int fun(int *num, int *sum)
{
(*num)++;
(*sum)++;
return *num + *sum;
}
int main(int argc, char const *argv[])
{
int a = 3, b = 4;
int ret = fun(&a, &b);
printf("%d %d %d\n", a, b, ret); // 4 5 9
return 0;
}
-
- 数组传递
和地址传递一样,参数中存在数组的定义,它也会认为是一个指针
#include <stdio.h>
char *fun2(char a[32])
{
a = "hello";
char *str = "hello";
// 如果是数组的话是 32, 如果是指针的话是4
printf("%d\n", sizeof(a)); // 4
return a;
}
int main(int argc, char const *argv[])
{
char *ch = fun2("abc");
printf("%s\n", ch);
char *str = "hello";
return 0;
}
补充:
char *p = "hello";
// p 在栈区开辟4字节空间存放字符串常量“hello”的首地址
// “hello“:存放在常量区 // 所以是不能被修改的
char buf[32]="hello";
//buf:在栈区开辟32字节空间,存放"hello"字符串
// 所以当时说怎么去存放字符串,使用字符串数组,可以遍历修改
例如
char * GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str=NULL;
str = GetMemory();
printf(str);
}
请问运行 Test 函数会有什么样的结果?
答:编译时会报警告,提示局部变量;运行结果不同的编译器环境下,可能会有不同的结果,可能会出现打印乱码或正常输出;也有编译器会出现段错误
动态内存开辟(开辟堆区空间)
为什么存在动态内存开辟
<1>在技术方面,普通的空间申请,都是在全局或者栈区,全局一般不太建议大量使用,而栈空间有限,那么如果一个应 用需要大量的内存空间的时候,需要通过申请堆空间来支持基本业务。
<2>在应用方面,程序员很难一次预估好自己总共需要花费多大的空间。想想之前我们定义的所有数组,因为其语法约束,我们必须得明确"指出"空间大小.但是如果用动态内存申请(malloc)因为malloc是函数,而函数就可以传参,也就意味着,我们可以通过具体的情况,对需要的内存大小进行动态计算,进而在传参申请,提供了很大的灵活性
开辟空间
#include <stdlib.h>
void *malloc(size_t size);
功能:在堆区开辟空间
参数:size:开辟空间的大小 (单位:字节)
返回值:
成功:返回开辟空间的首地址
失败:NULL
释放空间
#include <stdlib.h>
void free(void *ptr);
功能:释放堆区空间
参数:ptr:堆区空间的首地址
返回值:无
free(p);
// 对p进行释放的时候需要对 p 赋值一个NULL,避免它成为野指针
p = NULL;
例子:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
// 为开辟n个数据类型的大小
int n;
scanf("%d", &n);
int *p = (int *)malloc(n*sizeof(int));
// 容错判断
if(p == NULL)
{
printf("开辟失败\n");
return -1;
}
printf("开辟成功\n");
// 使用堆区空间
for (int i = 0; i < n; i++)
{
*(p+i) = i;
}
// 释放堆区空间
free(p);
p = NULL;
return 0;
}
注意:
1.手动开辟堆区空间,要注意内存泄漏
当指针指向开辟堆区空间后,又对指针重新赋值,则没有指针指向开辟带队去空间,就会造成内存泄漏
2.使用完堆区空间后及时释放空间
思考:如下代码的输出结果
char *fun(char *p)//p=NULL
{
p = "hello";
// 这个就是把 hello 赋值到 p 里面去了
return p;
}
main()
{
char *m = NULL;
m = fun("abc");
printf("%s\n", m);
}
代码出现段错误,原因?
解决方案:
总结:如果在子函数中开辟堆区空间,想在主函数中拿到堆区空间首地址有两种方法:
-
- 通过返回值
char * fun()
{
char *p = (char *)malloc(32);
strcpy(p, "hello");
return p;
}
int main()
{
// 定义一个指针接收返回值
char *m = fun();
printf("%s\n",m);
free(m);
m = NULL;
return 0;
}
-
- 通过传参
void fun(char **p) // &m
{
*p = (char *)malloc(32); // m
strcpy(*p, "hello");
}
int main()
{
char *m = NULL;
fun(&m);
printf("%s\n", m);
free(m);
m=NULL;
return 0;
}
string 函数族
- strcpy
#include <string.h>
char *strcpy(char *dest, const char *src);
功能:实现字符串的复制
参数:dest:目标字符串首地址
src:源字符串首地址
返回值:目标字符串首地址
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[])
{
char s[32];
char a[32] = "hello";
strcpy(s, a);
printf("%s\n", s);
return 0;
}
复制包括\0
char *strncpy(char *dest, const char *src, size_t n);
功能:实现字符串的复制
参数:dest:目标字符串首地址
src:源字符串首地址
n:字符个数
返回值:目标字符串首地址
复制src前n个字符
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[])
{
char s[32] = "world";
char a[32] = "hello";
strncpy(s, a, 2);
printf("%s\n", s);
return 0;
}
- strlen
#include <string.h>
size_t strlen(const char *s);
功能:计算字符串的实际长度
参数:s:字符串的首地址
返回值:实际长度
- strcat
#include <string.h>
char *strcat(char *dest, const char *src);
功能:用于字符串拼接
参数:dest:目标字符串首地址
src:源字符串首地址
返回值:目标字符串首地址
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[])
{
char s[32] = "hello";
char a[32] = "world";
strcat(s, a);
printf("%s\n", s);
return 0;
}
char *strncat(char *dest, const char *src, size_t n); 拼接src的前n个字符
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[])
{
char s[32] = "hello";
char a[32] = "world";
strncat(s, a, 2);
printf("%s\n", s);
return 0;
}
- strcmp
#include <string.h>
int strcmp(const char *s1, const char *s2);
功能:用与字符串比较
参数:s1、s2 用于比较字符串的首地址
返回值:
从字符串首个字符开始比较字符的 ASCII的大小,如果相等继续向后比较
1 s1 > s2
0 s1 == s2
-1 s1 < s2
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[])
{
char s1[] = "hello";
char s2[] = "hellohello";
char *s3 = "nihao";
char *s4 = "shijie";
char *s5 = "hello";
int ret = strcmp(s1, s2); // s1 < s2 = -1
int ret1 = strcmp(s1, s3); // s1 < s3 = -1
int ret2 = strcmp(s2, s4); // s2 < s4 = -1
int ret3 = strcmp(s3, s2); // s3 > s2 = 1
int ret4 = strcmp(s1, s5); // s1 == s5 = 0
if(!strcmp(s1, s5)) !false == true
{
printf("相等\n");
}
return 0;
}
int strncmp(const char *s1, const char *s2, size_t n); 比较前n个字符的大小
递归函数
- 定义:自己调用自己
- 执行过程分为两个阶段
- 递推阶段:从原问题出发,按递推公式从未知到已知最终达成递归的终止条件
- 回归阶段:按递归的终止条件求出结果,逆向逐步带入递归公式,回到原问题求解
- 递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次调用之后越来越接近这个限制条件
- 大事化小
例子:
求一个5-1的乘积
#include <stdio.h>
int fun(int n)
{
if(n == 1)
{
return 1;
}
return n*fun(n-1);
}
int main(int argc, char const *argv[])
{
printf("%d\n", fun(5));
return 0;
}
例子:打印一个数的每一位
接收一个整型值,按照顺序打印它的每一位
示例:1234
输出:1 2 3 4
思路:按照顺序打印它的每一位我们就用 1234%10就会等于 4,想打印其他的数1234怎么来呢,1234/10 = 123,再继续 123%10等于3,以此类推
#include <stdio.h>
void fun(int n)
{
if(n > 9)
{
fun(n / 10);
}
printf("%d\n", n % 10);
}
int main(int argc, char const *argv[])
{
fun(1234);
return 0;
}
结构体
- 定义:
用户自定义的数据类型,在结构体中可以包含若干个不同数据类型的成员变量(也可以相同),使这些数据项组合起来反映某一个信息
- 格式:
struct 结构体名 (用户自定义的数据类型)
{
数据类型 成员变量1;
数据类型 成员变量2;
数据类型 成员变量2;
};
- 结构体变量:
- 概念:
通过结构体类型定义的变量
-
- 格式:
struct 结构体名 变量名;
-
-
- 先定义结构体,在定义结构体变量
-
struct 结构体名
{
成员变量;
};
struct 结构体名 变量名;
#include <stdio.h>
struct student
{
char name[32];
int id;
int age;
};
int main(int argc, char const *argv[])
{
struct student stu;
return 0;
}
-
-
- 定义结构体的同时,定义结构体变量
-
struct 结构体名
{
成员变量;
} 变量名;
struct student
{
char name[32];
int id;
int age;
} stu;
-
-
- 缺省结构体名定义结构体变量
-
struct
{
成员变量;
} 变量名;
- 赋值:
- 定义变量的同时直接用大括号赋值
#include <stdio.h>
struct student
{
char name[32];
int id;
int age;
};
int main(int argc, char const *argv[])
{
struct student stu = {"zhangsan", 153461, 18};
return 0;
}
-
- 定义变量时未初始化,然后对变量单独赋值
#include <stdio.h>
#include <string.h>
struct student
{
char name[32];
int id;
int age;
};
int main(int argc, char const *argv[])
{
struct student stu;
stu.age = 18;
stu.id = 3517635;
strcpy(stu.name, "zhangsan");
return 0;
}
-
- 点等法赋值
#include <stdio.h>
#include <string.h>
struct student
{
char name[32];
int id;
int age;
};
int main(int argc, char const *argv[])
{
struct student stu = {
.id = 361867,
.age = 18,
.name = "zhangsan",
};
return 0;
}
- 访问
通过 . 访问:结构体变量名.成员变量名
scanf("%s %d %d", stu.name, &stu.age, &stu.id);
printf("%s %d %d\n", stu.name, stu.age, stu.id);
- 重定义 typedef
typedef int int_num;
int a; == int_num a;
typedef int * int_p;
int *p = &a; == int_p p = &a;
-
- 定义结构体的同时重定义
typedef struct student
{
int id;
int age;
} STU; // STU 是结构体类型重定义的名字
struct student stu; == STU stu;
-
- 先定义结构体,然后重定义
struct student
{
int id;
int age;
};
typedef struct student STU;
STU stu;
例子:
typedef struct student
{
int id;
int age;
} STU;
STU stu = {1, 18};
STU *p = &stu; // struct student * p = &stu
int a;
typedef int STU
STU *p = &a;
typedef struct student
{
int id;
int age;
} STU, *STUP;
typedef struct student STU
typedef struct student * STUP
STU stu = {1, 21};
STUP p = &stu;
STU *q = &stu; // STUP p = &stu;
练习:创建一个名为student的结构体,包含姓名,学号,班级,分数,(数据类型自己定义),从终端输入学生的信息并打印。
练习:用指针实现strcpy、strcat
strcpy:
#include <stdio.h>
char *mystrcpy(char *s1, char *s2)
{
char *p = s1;
while (*s2 != '\0')
{
*s1 = *s2;
s1++;
s2++;
}
*s1 = '\0';
return p;
}
int main(int argc, char const *argv[])
{
char str[32] = {};
char arr[32] = "hello";
mystrcpy(str, arr);
printf("%s\n", str); // hello
return 0;
}
结构体数组
概念
结构体类型相同的变量组成的数组
格式:
2.1. 定义结构体的同时定义结构体数组
struct student
{
int id;
int age;
} stu[3];
2.2. 先定义结构体,然后定义结构体数组
struct student
{
int id;
int age;
};
struct student stu[3];
- 初始化
3.1 定义结构体数组同时赋值
struct student
{
int id;
int age;
} stu[3] = {
{3415341, 21}, // 这是第一个人的信息
{35134, 18}, // 这是第二个人的信息
{763163, 23} // 这是第三个人的信息
};
3.2. 先定义结构体数组,在对结构体数组的每一个元素分别赋值
struct student
{
int id;
int age;
} stu[3];
stu[0].id = 1;
stu[0].age = 18;
stu[1].id = 2;
- 结构体数组的大小
sizeof(结构体数组名);
元素个数*结构体类型大小;
- 结构体数组输入输出(for循环)
#include <stdio.h>
struct student
{
int id;
int age;
float score;
char *name;
} stu[3];
int main(int argc, char const *argv[])
{
for (int i = 0; i < 3; i++)
{
scanf("%d %d %f", &stu[i].id, &stu[i].age, &stu[i].score);
}
for (int i = 0; i < 3; i++)
{
printf("%d %d %.2f\n", stu[i].id, stu[i].age, stu[i].score);
}
return 0;
}
练习: 创建一个名为student的结构体数组,包含学号,姓名,分数,(数据类型自己定义),从终端输入学生的信息并打印分数及格的学生信息(输入3人即可)。
结构体指针
- 概念:
指向结构体的指针
- 格式:
struct 结构体名 *结构体指针名
#include <stdio.h>
struct student
{
int id;
int age;
} stu1, stu2;
struct work
{
int id;
int age;
} w1, w2;
int main(int argc, char const *argv[])
{
struct student *p1 = &stu1;
struct student *p2 = &w1; // 错误,结构体类型不匹配
return 0;
}
- 赋值
格式:结构体指针变量名 -> 成员变量名
-> 指向的
#include <stdio.h>
struct student
{
int id;
int age;
} stu1, stu2;
int main(int argc, char const *argv[])
{
struct student *p1 = &stu1;
p1 -> id = 1;
p1 -> age = 18;
printf("%d %d\n", p1 -> id, p1 -> age);
return 0;
}
(*p1).成员变量
练习:创建一个结构体数组,数组名为student,成员包含学号,姓名,成绩(数据类型自己设定)从终端输入学生信息,封装函数实现按成绩从低到高打印学生信息。
- 结构体指针大小
本质是指针,4字节
总结
- 不能把结构体类型变量作为整体引用,只能对结构体类型变量中的成员变量分别引用
- 如果成员变量本身属于另一种结构体类型,用若干个成员运算符一级级找到你想要的成员变量
- 可以把成员变量当成普通变量运算
- 在数组中,数组之间不能彼此赋值,结构体变量可以相互赋值
int a[3] = {};
int b[3] = {1, 2, 3};
a = b;
结构体大小
sizeof(struct 结构体名); // 结构体类型大小
struct stu
{
char a;
short w;
char y;
int b;
char c;
};
sizeof(struct stu); // 12
结构体大小遵循字节对齐原则
- 字节对齐
在实际使用中,访问特定数据类型变量时需要在特定的内存起始地址进行访问,这就需要各种数据类型按照一定的规则在空间上进行排列,而不是顺序地一个接一个地存放,这就是字节对齐
- 字节对齐原则
- 在32位系统下默认的value值为4字节,判断结构体类型中最大成员变量的字节大小,和默认的value值进行比较,按小的数进行对齐
- 结构体成员进行对齐时遵循地址偏移量是成员类型的整数倍,double 类型数据存放在4字节的整数倍
- 结构体成员按顺序进行存储,如果不满足以上条件时,需要填充空字节
- 为什么要进行字节对齐?
- 平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。(提高程序的移植性)
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
- 内存原因:在设计结构体时,通过对齐规则尽可能优化结构体的空间大小至最小空间
例子:
struct S
{
double a;
char b;
int c;
}
第一步:找出成员变量的大小,让其与编译器的默认的对齐数进行比较,取较小的值作为该成员变量的对齐数
注:我们现在使用的编译器默认对齐数为4
VS编译器默认对齐数为8
第二步:根据每个成员对应的对齐数画出他们在内存中的相对位置
第三步:通过最大对齐数决定最终该结构体的大小
通过图片我们可以知道。绿色部分+红色部分紫色部分+红色与紫色之间的白色部分(浪费掉的)总共占用了16字节的内存空间
补充:
指针数组:
命令行参数:
int main(int argc, char const *argv[])
{
return 0;
}
argv:就是一个指针数组,里面存方法的是命令行传递的字符串
argc:表示 argv 指针数组里面存放数据的个数,即命令行传递字符串的个数
共用体
不同类型的成员变量共用一块地址空间
- 格式:
union 共用体名
{
成员列表;
};
- 定义共用体变量
union 共用体名 变量名;
union val
{
int a;
char ch;
}
union val v;
v.a = 10;
// 他俩不能同时出现,是以最后一次赋值为准
v.ch = 'a';
- 特性:
- 共用体成员共用同一块地址空间
- 赋值顺序以最后一次为准
- 共用体大小为成员中类型最大的数据的大小
- 使用共用体测试大小端
#include <stdio.h>
union val
{
int num;
char ch;
};
int main(int argc, char const *argv[])
{
union val v;
v.num = 0x12345678;
if(v.ch == 0x78)
{
printf("小端\n");
}
else
{
printf("大断\n");
}
return 0;
}
枚举
维基百科的理解:枚举类型用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型。 定义:是指将变量的值一一列出来,变量的值只限于列举出来的值的范围内。
枚举类型和宏定义是差不多的,只有细微区别,宏运行是在预处理阶段完成的,枚举类型是在与编译阶段完成的。
- 定义:
用户自定义数据类型,用于声明一组常数
- 格式:
enum 枚举名
{
value1,
value2,
value3,
.....
};
#include <stdio.h>
enum week
{
MON,
TUE,
WED
};
int main(int argc, char const *argv[])
{
printf("%d %d %d\n", MON, TUE, WED); // 0 1 2
int n;
scanf("%d", &n);
switch (n)
{
case MON:
printf("周一\n");
break;
case TUE:
printf("周二\n");
break;
case WED:
printf("周三\n");
break;
}
return 0;
}
未赋初值,第一个常数会默认为0,依次加一(如果第一个值被设为1,则默认从1开始递增)。
存储类型
auto static extern register
- auto
修饰变量,一般省略时会认为是auto类型
- static:修饰变量和函数
修饰变量:
- 变量存在全局区(静态区)
如果静态局部变量有初值,存放在.data区,没有初值存放在 .bss 区域
- 生命周期为整个程序
- 限制作用域:
- 修饰局部变量,和普通的局部变量的作用域没有区别,但是生命周期被延长为整个程序
- 修饰全局变量,限制在本文件中使用
- 只初始化一次,初值赋值0;
#include <stdio.h>
void fun()
{
static int a = 0;
a++;
printf("%d\n", a);
}
int main(int argc, char const *argv[])
{
fun(); // 1
fun(); // 2
return 0;
}
修饰函数:
static 修饰函数,限制在本文件中使用
extern:外部引用
通过 extern可以引用其他文件的全局变量或函数
register:寄存器类型
由于寄存器数量比较少,申请不到空间时和auto一样