一些C语言相对进阶知识点的总结
目录
一、指针基础
二、函数
三、文件
四、结构体
五、预处理
六、内存布局
1、 指针概念:
类型符 *指针变量 例如:int *p;
p:指针变量;*p:指针指向的地址的值;
**p:指向指针的指针:
p是二级指针变量,其中存放着i的地址0x00000004,p也有地址,是0x00000000;
*p= i= 0x00000008; //p解引用也就是i的内容
**p= *i = "一段内容"; //i解引用,也就是i指针指向的d地址上的值
p= &i = 0x00000004; //p存的是i的地址,i的地址是0x00000004
&p = 0x00000000; //p取地址
//“ * ”用法:1、表示乘法,例如c=a*b;
2、定义指针变量,例如:int *p,a;其中p是指针变量,a是整型变量。
3、表示获取指针指向的数据,是一种间接操作,例如:int*p=&a,*p=100;
//指针变量保存的是地址本质上也是个整型数!所以指针变量可以进行部分运算,例如加、减、比较等.
2、数组指针和指针数组。
数组指针:类型名 (*数组名)[元素个数] 例如:int(*p)[3];
- 这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址.
- 常常用于地址传参,用来接收二维数组的首地址
指针数组:类型名 *指针名 [元素个数] 例如:int *p[3];
- 可以用于记录多个字符串,快捷使用。
- 指针数组常用在主函数传参,在写主函数时,参数有两个,一个确定参数个数,一个这是指针数组用来接收每个参数(字符串)的地址。
- 如果是向子函数传参,这和传递一个普通数组的思想一样,不能传递整个数组过去,如果数组很大,这样内存利用率很低,所以应该传递数组的首地址,用一个指针接收这个地址。因此,指针数组对应着二级指针
3、函数指针和指针函数。
函数指针:指向函数入口地址的指针。
数据类型 (* 指针变量名)(函数参数表) 例如: int (*p) (int ,int);
- 函数指针的值就是指针所指向的函数的返回值
- int (*p) (int ,int);仅仅定义了一个指针,它可以指向不同的函数,只要该函数返回一个整型数据,且有两个整型参数。
- p+n、p++、p--无意义
指针函数:函数返回值是指针的函数。
4、结构体指针:是该结构体变量所占据内存段的起始地址。也可以用来指向结构体数组中的元素。
struct 结构名 *结构指针变量名 例如 struct student *p;
5、文件指针:指向一个包含文件信息的结构体,
包括: 缓冲区的地址 缓冲区中当前字符的位置 文件的访问模式
FILE *fp;
6、指针访问数组的方式
p++
(*p).成员名称
p->成员名称
p+i
(*(p+i)).成员名称;
(p+i) -> 成员名称
p[i]
p[i].成员名称
//函数可以通过多文件编程使用存放在其他C文件里的函数,使用时该函数所在的文件需要声明头函数。
1.内置函数:编写程序时可以直接使用的函数,是C库中自带
2.自定义函数:用户自己定义的可以实现用户需要的功能的函数,使用前如果没有定义需要提前声明。
3.嵌套和递归:函数也可以嵌套和递归使用,但是在使用的时候需要留一个跳出嵌套或者递归的出口。
4.返回值:被使用函数运行结束后返回到使用函数的值,类型为函数名定义,如果返回值和返回值类型不匹配,以返回值类型为准。一次最多返回一个值。在被使用函数中起到的作用多为结束函数。关键词为:return 返回值;
//注:无返回值函数(void 函数)没有返回值
5.传参:
a.数值传参:将主函数中的参数的数值传递到被使用函数中去;
例如:
1 #include
2
3 void sum_passed(int a)
4 {
5 a=10;
6 printf("被使用函数中的a=%d\n",a);
7 }
8 int main()
9 {
10 int a=0;
11
12 sum_passed(a);
13
14 printf("主函数中的a=%d\n",a);
15 return 0;
16 }
得到的结果是
被使用函数中的a=10
主函数中的a=0
b.地址传参:将主函数中元素的地址传递到被使用函数中,这样可以直接通过地址改变地址所保存的数。也就是说,如果被使用函数可能需要改变主函数中参数的值,就把主函数中参数的值的地址传递过去。
例如:
#include
2
3 void sum_passed_1(int a)
4 {
5 a=10;
6 printf("被使用函数中的a=%d\n",a);
7 }
8 void address_passed(int *p)
9 {
10 *p=20;
11 printf("被使用函数中的*p=%d\n",*p);
12 }
13 void sum_passed_2(int *q)
14 {
15 int b=30;
16 q=&b;
17 printf("指针之间的数值传参*q=%d\n",*q);
18 }
19 int main()
20 {
21 int a=0;
22 int *p=&a;
23
24 address_passed(&a);
25 printf("主函数中的a=%d\n",a);
26
27 sum_passed_2(p);
28 printf("主函数中的*p=%d\n",*p);
29 return 0;
30 }
结果是
被使用函数中的*p=20
主函数中的a=20
指针之间的数值传参*q=30
主函数中的*p=20
注意这里主函数的*p和被调用函数的*q的值不同,为什么呢?
调用函数前
调用函数时
函数结束后
于是就出现了*p*q不同值得情况
如果需要避免这个情况,只需要使用二级指针作为函数形参,道理和一级指针接收变量一致
数组作为参数进行传参,理同指针,直接传递数组首地址,形参使用指针,使用p[]或者*p接收数组首地址,如果实参是二维数组就使用数组指针(*p[])接收。
6.局部变量和全局变量:
局部变量 | 静态局部变量 | 全局变量 | 静态全局变量 | |
生存期 | 变量定义 到 函数结束 |
变量定义 到 程序结束 |
程序开始 到 程序结束 |
程序开始 到 程序结束 |
作用域 | 函数内部 | 函数内部 | 所有.c文件 | 仅本c.文件 |
生存期: 变量定义 ----> 变量销毁
作用域: 变量的可访问性
//FILE*fp
1.打开文件
fp=fopen(“./file”,”w\r\a”);
if(NULL ==fp)
{
perror("fopen");
return -1;
}
2. 文件的格式化输入输出
fprintf(fp, "%d\t%s\t%lf\n", s.num, s.name, s.score);
fp :指针指向的文件;
"%d\t%s\t%lf\n":输出的数据格式;
s.num, s.name, s.score:输出的数据的地址。
ret = fscanf(fp, "%d%s%lf", &temp.num, temp.name, &temp.score);
if(EOF == ret) //到达文件结尾,EOF可以视为-1
{
break;
}
fp :指针指向的文件;
"%d%s%lf":输入的数据格式;
&temp.num, temp.name, &temp.score:输入的数据在程序中保存的地址。
if():判断是否输入结束。
3. 字符串读写文件
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
函数功能:以字节的方式对文件作写操作
ret = fwrite(a, sizeof(int), 5, fp);
if(ret < 5) {perror("fwrite");return -1;}
参数说明:
ptr: 要写的数据的起始地址
size: 数据的单位大小
nmemb: 个数
stream: 文件指针
返回值(ret): 成功返回正确写进去的数据个数
失败小于指定的个数
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
函数说明:以字节的方式读取数据 将本地文件中的数据加载到内存中
ret = fread(&temp, sizeof(int), 1, fp);
if(0 == ret) //到达文件结尾
参数说明:
ptr: 存放读数据内存的起始地址
size: 数据的单位大小
nmem: 读取的个数
stream: 文件指针
返回值: 成功返回正确读到的数据个数
到达文件结尾返回0
ps: 正常情况下,文件中数据的个数是不知道的, 但文件中数据的类型是确定的。
所以,读数据一般都是从文件头开始,一个数据一个数据的读取,到达文件尾部结束!
3. 关闭字符串文件
fclose(fp):fp是指向你要关闭文件的指针。
1.定义结构体:本质就是自定义一种数据类型。(type 代指类型名)
struct 结构体名称
{
type1 成员名1;
type2 成员名2;
....
typen 成员名n;
};
可以理解成构建一个个体,其中的“type”是你描绘的个体的各方面属性。
2.定义结构体变量
struct 结构体变量 变量名
这个变量就是你定义的一个个体。
3.访问变量空间
结构体变量.成员名称: 表示结构体变量中的某个成员
注:定义没有名称的结构体是合法的, 但是其他地方没有办法使用,只能一边定义结构体,一边定义变量
4.结构体数组:和普通数组一样,作为一个数组内部包含的都是结构体变量。
5.嵌套结构体: 在一个结构体中嵌套定义其他类型的结构体成员变量.
注:1、结构体中的成员必须是已经存在
2、如果A结构体中嵌套B类型的结构体成员变量, 应该将B结构体在A之前定义
3、结构体变量只能访问自己的成员, 如果有嵌套结构体成员变量,那么先访问嵌套的结构体成员变量,然后再通过这个变量去访问自己的成员!!
6.结构体与函数:和变量或者指针与函数的关系一致。
7.结构体对齐原则:
1、每个成员按照自己的字节数对齐
2、结构体对齐数就是最大成员的对齐数
3、如果最后总的字节数不是结构体对齐数的整数倍, 那么加到第一个整数倍为止
4、数组按照元素的大小对齐
8.typedef : 给已经存在的数据类型取别名
typedef 存在的数据类型名称 别名;
9.联合体:
根据最大成员分配内存, 但最后总的字节数是对齐数的整数倍
最大成员的对齐数就是联合体的对齐数
1.宏定义:#define 宏名 宏体
a.无参宏定义: #define NUM 10
1、宏名尽量大写,区分变量名
2、宏定义只做简单的替换,不做任何运算
3、宏体建议添加括号
4、#undef 宏名 :强制结束宏定义的生命周期
5、宏体可以省略,表示该宏已经定义过!
b.带参宏定义:#define S(a,b) ((a)*(b))
1、参数不需要加类型的
2、参数表和宏名之间不能有空格
2.条件编译。
#if 标识符
语句块
#endif
#if 标识符
语句块1
#else
语句块2
#endif
#if 标识符1
语句块1
#elif 标识符2
语句块2
#elif 标识符3
语句块3
.......
#else
语句块2
满足条件()进入语句块 #endif作为结束标志,如果条件都不满足也需要执行语句,可以用#else执行语句。
这个编译方式和if语句有些相像,后面就只放最基础的版本了。
#ifdef 标识符
语句块
#endif
标识符定义了进入,没定义进入下一个分支。
#ifndef 标识符
语句块
#endif
标识符没定义进入,定义了就进入下一个分支。
3.头文件包含“.c”
1、xx.c 称为源文件, 存储具体函数实现
xx.h 称为头文件,存储对应模块函数声明、宏定义、结构体定义、全局变量声明2、一个模块: 包含两部分 源文件+头文件, 文件名相同, 后缀不同
3、包含标准头文件使用 <>
包含自己写的头文件 ""4、防止头文件重复包含:
(1) 方法一:
#ifndef _头文件名大写_H
#define _头文件名大写_H
语句块
#endif
(2)方法二:
#pragma once
5、如果头文件不在同一个目录,如果包含头文件:
(1)方法一:
#include "具体路径"(2)方法二:
编译时提供头文件所在的目录路径:gcc xx.c xx.c -o xx -I 头文件目录路径
4.常用关键字
const: 防止变量被恶意修改
int const *const p = &a;//指针的指向和指针指向的内容都不能发生改变
const int *const p = &a;//同上
enum:枚举
1、枚举也是自定义的一种数据类型,定义一组常量, 默认从0开始, 后面值依次递增
2、也可以重新设置常量的值
3、枚举是占内存空间的, 可以使用gdb 查看: p /d 常量名4、define 一次只能定义一个常量,并且要手动设置值, 不占用内存空间,无法使用gdb查看
#endif
enum File_Error
{
OPEN_ERROR ,
CLOSE_ERROR,
READ_ERROR ,
WRITE_ERROR,
OTHER_ERROR,
};
static:修饰静态变量
1、修饰变量: 静态变量
静态局部变量和静态全局变量生存期: 程序开始----》程序结束
静态局部变量作用域是定义函数内部, 静态全局变量作用域是本.c文件2、修饰函数: 内部函数
static void test();
extern
1、全局变量的声明
2、外部函数的声明
extern int g_num; //全局变量的声明
extern void func();
a.代码区:程序被操作系统加载到内存的时候,所有的可执行代码都加载到代码区,也叫代码段,这块内存是不可以在运行期间修改的。
b.静态区:所有的全局变量以及程序中的静态变量,以及字符串都存储到静态区, C编译器对于相同的字符串会自动进行优化分配为统一的地址。
c.栈区:栈stack是一种先进后出的内存结构,所有的自动变量,函数的形参都是由编译器自动放出栈中,当一个自动变量超出其作用域时,自动从栈中弹出。
d.堆区:堆heap和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序。堆是一个大容器,它的容量要远远大于栈,但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成
堆空间注意事项:
1、内存越界
2、内存泄露3、二次释放(释放完指针仍然指向原来的堆空间,推荐释放完后指针指向NULL)