一、函数
函数的基本概念:
- 有特定功能的一小段程序
- C语言基本构成单位
函数的分类:
- 标准C定义的标准库函数
- 第三方库函数
- 自定义函数
函数的定义:
定义格式:返回值类型 函数名(参数类型 参数名……)
说明:定义函数主要考虑三个问题:
- 函数的主要功能
- 需要的外部参数
- 返回结果
函数的调用:
-
使用条件
- 定义在调用者函数之前,可直接调用
- 定义在调用者函数之后,需要先声明函数声明
- 声明时,可以没有变量的名字,但必须要有类型 int FUN(int ,int )
-
注意事项(在一个函数中调用另一个函数需具备以下条件)
- 被调用函数存在
- 使用库函数,需添加头文件 #include <xxx.h>
- 使用自定义函数,而该函数在主调函数之后(在同一文件),需声明
- 即使没有实现函数,但有声明,在写的时候没有错误,但在编译链接时会出错(无法解析XXX外部符号)
- 将函数作为另一个函数的参数时,该函数返回值类型与当前函数参数类型一致
-
函数语句
把函数调用作为一个语句。这时不需要函数带返回值,只要求完成一定操作
-
函数表达式
函数出现在一个表达式中,此时需要函数返回一个确定的值参加运算
-
函数参数
函数调用作为一个函数的实参
二维数组作为函数参数的正确写法:
void Func(int array[3][10]);
void Func(int array[ ][10]);
形式参数和实际参数:
什么是实参?什么是形参?
调用函数的时候,有调用者和被调用者之分,两者之间存在数据传递关系。
定义一个函数时,括号里的参数就是形式参数,简称形参
当一个函数需要调用另一个函数,此时需要传递参数,该参数是实际参数,简称实参
//定义一个加法函数,参数a,b是形参
int add(int a,int b)
{
return a+b;
}
int main()
{
add(3,4); //调用add函数,此时传递的参数3,4是实参
}
说明:
- 实参可以是常量、变量或者表达式,但要求他们有确定的值
- 实参和形参的类型必须一致
- 单向传递,只能由实参传递给形参,而不能形参传递给实参
- 形参不能改变实参的值
二、作用域
在C语言中,作用域分为三种:文件作用域、函数作用域、复合语句作用域
变量类型分为:局部变量、全局变量、静态局部变量
局部变量:
- 定义在函数内部,只对当前函数有效
- 如果
{}
里面定义的局部变量与外面的重名,则以内部为准
全局变量:
- 定义在函数外部,对所有函数有效
- 作用范围:可以被在其定义位置之后的所有函数所共享
静态局部变量:
- 所有的全局变量均为静态变量
- 局部静态变量需加修饰符stasic
- 静态变量的特点:在程序的整个执行过程中始终存在,但是在他作用域外不能使用。静态变量的生存期就是整个程序的运行期,函数体内如果在定义静态比变量的同时初始化,则以后程序不再进行初始化操作。
#include <stdio.h>
#include <stdlib.h>
//局部静态变量
void fun()
{
/*局部静态变量,只会初始化一次,每次离开函数时,还保留着当时的值,再次进入函数时,值是上一次留下的*/
//static int nNum = 100;
int nNum = 100;
printf("%d\n", nNum);
nNum++;
}
int main()
{
fun();
fun();
fun();
system("pause");
}
/*输出结果:100 100 100
使用静态局部变量-输出结果:100 101 102*/
extern 与 static 在全局变量中的应用:
- 如果组成一个程序的几个文件需要用到同一个全局变量,只要在其它引用该全局变量的源程序文件中说明该全局变量为extern即可
- 如果一个源程序文件中的全局变量仅限于该文件使用,只要在该全局变量定义时说明加static即可
三、预处理
编译预处理:主要用于在程序正式编译前进行一些预加工操作,所有的编译预处理命令均以#
开头,编译预处理共分为几个方面:
- 宏定义
- 文件包含
- 条件编译
宏:宏定义的作用就是在程序中某段代码的一个别名,主要是为程序调试、移植等提供便利,所有的宏命令都以符号#define
开头,并且结尾不用分号。
#define PI 3.14
#undef PI
宏定义的作用就是在编译预处理时,将源程序中所有标识符替换成语句序列
- 宏名一般大写
- 有效范围:从定义位置到文件结束。如需要终止宏定义作用域,可以用#undef命令
无参宏:指执行单一替换功能的宏定义
注意事项:
- 宏定义时可以引用已经定义的宏名
- 对程序中用双引号括起来的字符串内的字符,不进行宏的替换操作
有参宏:可以让我们在定义宏时带参数扩大宏的应用范围。
格式:#define 标识符(参数表) 字符串
作用:在编译预处理时,将源程序中所有标识符替换成字符串,并且将字符串中的参数用实际使用的参数替换
注意:
- 宏名和参数之间无空格
#define L(r) 2*PI*r
源程序 L(2+3)-->L(2*PI*2+3)不是L(2*PI*(2+3))
解决办法是:#define L(r) 2*PI*(r)
#define CUBE(x) (x*x*x)
int main()
{
int a = 0, b = 2;
a = CUBE(b + 1); // 错误 a=(b*b*b+1)
printf("%d", a); // 正确 a = (b + 1*b + 1*b + 1);
system("pause");
}
文件包含:
- 标准方式 #include <xxxx.x>
只按照标准C方式在C语言编译器的C函数库头文件中查找要包含的文件 - 通用方式 #include “xxxx.x”
先在源文件所在的目录中查找要包含的文件,若未找到,按标准方式查找
条件编译:
四、指针
定义变量的本质:
- 开辟一块内存空间,并用变量名代表那块内存空间
- 内存空间的最小单位是字节。每一个字节都有一个编号,这个编号我们称为地址。
- 有一种特殊的变量,专门存储地址,这种变量叫做指针变量。指针变量也有一块内存空间,保存的是指向目标变量的地址。
指针变量的基本使用方式:
- 定义指针变量
- 给指针变量赋值
- 指针解引用
在这个过程中需要使用两个运算符:取地址运算符&
和解引用运算符*
定义指针变量:
定义格式:类型名 *指针变量名
int *pNums;
注意:
- 变量名前
*
是一个说明符,用来说明该变量是指针变量,这是*
不能省略,但是他不是变量名的一部分 - 类型名表示指针变量所指向的变量的类型,而且只能指向这种类型的变量
给指针变量赋值:
int a = 12;
int *p = 0;
p = &a; //&a表示a的地址
int temp = *p; //等价于 temp =a;
*p = 200; //等价于 a=200;*p 表示指针变量p指向的变量
注意事项:
- 指针变量是用来存放地址的,不要给指针变量赋常数值,如 int *p = 1000;
- 指针变量没有指向明确的地址前,不要对它所指的对象赋值,如 int b = 5;*p = b;//类型不匹配
- 指针变量不能修改常量
指针解引用:
我们也知道,指针变量所对应的空间保存的是一个地址,我们要使用这个指针去修改指向变量的值,就需要用到解引用运算符*
int a = 12;
int *p = 0;
p = &a; //&a表示a的地址
//p等于a的地址
//解引用 *p等于a的内容
指针变量的打印:
//这需要根据你的printf函数的参数来决定。
//示例1
printf( “%d”,*p );//printf中的%d参数要求你提供一个整数,而p是个指针,它指向的是整数,这时用*p表示p指向的整数。如果你用p的话,将把指针的地址取值进行输出。
//示例2
printf( “%s”,p );//printf中的%s参数要求你提供一个指针,而p就是一个指针变量,可以直接写变量名p
//所以,参数使用时要满足printf对参数的要求。
指针与函数传参:
指针是间接引用,利用这一特性进行参数传递,使得函数内值的变化可以影响函数外部,指针传参的本质是地址传递。
指针的运算:
指针只能进行加法和减法运算:+ - ++ – += -=并且只要两种形式
- 指针±整数:指针加或减一个整数,得到的数据类型还是指针
- 指针-指针:得到的是这两个地址间能够存放多少个这种类型的数据
指针和二维数组:
二维数组名也是一个指针类型,叫做一维数组指针类型(数组名是数组类型的常量)
定义:类型名 (*变量名)[数组长度]
数组指针应该给出所指向数组的长度
int a[3][4];
int (*p)[4];
p=a; //a和p的类型是int (*)[4]
a[i]==*(a+i);
* a[i]+j==*(a+i)+j==&a[0][0]+i*3+j;
变化规律:
- 、对一维数组指针解引用会降维,变成1级指针
- 、对一维数组指针使用下标运算符也会降维,变为1级指针
- 、对于一维数组指针+1,会得到下一排起始地址,类型还是数组指针
- 、对一维数组名取地址,会变为二维数组指针,数值不变
实参 | 所匹配的形参 | ||
---|---|---|---|
数组的数组 | char c[8][10]; | char (*c)[10]; | 数组指针 |
指针数组 | char *c[10]; | char **c; | 指针的指针 |
数组指针(行指针) | char (*c)[10]; | char (*c)[10]; | 不改变 |
指针的指针 | char **c; | char **c; | 不改变 |
指针数组和数组指针:
指针数组:
- 本质是数组,里面元素为指针
- 数据类型 *指针数组名[常量表达式]
- 如:int *p[6]
数组指针:
- 本质是指针,指向一个数组
- 类型名 (*变量名)[数组长度]
- 如:int (*p)[6]
判断规则:
- 有*和[],[]优先级最高
- 变量名先和谁结合就是什么类型
区别:
int main()
{
/*char * color[] = { "RED","BLUE","GREEN" };
int i;
for (i = 0; i < 3; i++)
{
puts(color[i]);
}*/
//char color[][6] = { "RED","BLUE","GREEN" };
//char(*pcolor)[6] = color; //数组指针,指向color[][]数组的指针
//int i;
//for (i = 0; i < 3; i++)
//{
// puts(pcolor[i]);
//}
char color[][6] = { "RED","BLUE","GREEN" };
char* pcolor[3]; //指针数组,数组里有三个指针变量
int i;
for (i = 0; i < 3; i++)
{
pcolor[i] = color[i];
}
for (i = 0; i < 3; i++)
{
puts(pcolor[i]);
}
system("pause");
}
对于下面两个字符串,请从内存的角度说说他们的异同:
char *p1 ="hello"; //p1是一个指针
char p2[]="hello"; //p2是一个数组
p1是一个指针,指向了常量字符串,p1所指向的字符串内容是无法修改的,即p1[0]='1’会报错
p2是一个字符数组,有5个元素,存储了字符串”hello“,p2的内容是可修改的,p2[0]='1’改变了第一个字符
五、结构体和联合体
数组:同类型的多个数据集合体
结构体:不同类型数据的集合体
定义方式:
1.原始模式:
struct
{
类型名1 成员名1;
类型名2 成员名2;
}结构体变量名={初始化元素1,初始化元素2}; //定义时初始化
2.简便模式(常用):先构造出类型,然后使用类型名定义变量
struct _PERSON
{
int age;
char name[10];
};
typedef struct _PERSON PERSON;
typedef struct _PERSON* PPERSON;
//windows最常用写法
typedef struct _PERSON
{
int age;
char name[10];
}PERSON,*PPERSON;
PERSON per;
PPERSON pPer;
3.中间模式(不常用)
struct 结构体名
{
类型名1 成员名1;
类型名2 成员名2;
}结构体变量名;
结构体的初始化:
1.全部字段初始化:
//初始化时,初始值列表的值会依次初始化结构体中的每个字段,所以在填写初始化值的时候要注意数据类型是否匹配
struct person per[3] ={ {"xiaoming","M",20,175},
{"liuyuan","F",18,164},
{"zhaokui","M",22,188} }; //
2.部分字段初始化
//部分初始化,没有给出初始值的字段默认初始化为0
struct person per ={ 18 }; //
结构体变量的访问:
.
是成员运算符,它在所有运算符中优先级最高
如果是指针,访问结构体成员运算符为->
struct person per ={ 18 }; //
struct person* pPer =&per; //
per.name = "xiaoming";
//使用结构体指针访问结构体字段时,用->
pPer->name
pPer.naem//报错
结构体数组:定义结构体数组的方法和定义结构体变量的方法一样
struct person
{
char name[20];
char sex;
int age;
float height;
};
struct person per[3] ={ {"xiaoming","M",20,175},
{"liuyuan","F",18,164},
{"zhaokui","M",22,188} }; //
/*C语言编译器中,定义结构体时,struct person 前面的struct是必须的,但是在C++编译器中,这个不是必须的*/
联合体
联合体有时也被称为共用体,其基本使用语法,和结构体一样(只有关键字是union)
union 联合体名
{
类型名1 成员名1;
类型名2 成员名2;
};
结构体和联合体的区别:
- 结构体,每一个成员单独占用内存空间
- 联合体,所有的成员共享内存空间
六、堆内存
内存空间的动态分配:
- 定义一个指针变量
- 申请一片内存空间,并将首地址赋值给指针变量,此时便可通过指针变量访问这片内存
- 用完后释放这片内存空间
涉及函数:malloc和free
malloc:
- 功能:申请空间
- 参数:申请内存大小,字节数
- 返回值,申请出的空间的起始地址,类型void*
- 堆空间:内存数据是随机(debug版本默认以4个fd开头结尾,其他用cd来填充)
free:
- 功能:释放空间
- 只能释放malloc开辟出来的空间
- 不能重复释放相同地址
- 释放后记得将指针置为NULL,防止指针悬空
int *p = NULL;
p = (int *)malloc(sizeof(int)); //申请内存
free(p); //释放内存
使用堆和使用数组的区别:
- 数组长度是常量,堆大小可以是变量
- 堆需要自己释放,不释放或忘记释放,会造成内存泄漏。数组不需要自己释放
悬空指针:释放内存后,指针应及时的赋值为NULL。不赋值为NULL的话,称为悬空指针
和野指针:指针变量被定义后,没有被初始化,这种指针被称为野指针
指针总结:指针变量,要么指向有意义的地方(变量,数组,堆),要么就指向NULL(0)。
注意事项:
- 刚刚分配的动态内存的初始值是不确定的
- 不能对同一指针(地址)连续两次进行free操作
- 不能对指向静态内存区(全局变量)或栈内存区(局部变量)的指针应用free(但可以对空指针NULL应用free),对一个指针应用free后,它的值不会改变,但它指向了一个无效的内存区,这个是悬空指针
- 如果没有及时释放某块动态内存,并且将指向它的指针指向了别处,就会造成“内存泄漏”,执行malloc和free函数有一定的代价,所以对于较小的数据量不应该在动态内存之中,并且尽量避免频繁的分配和释放内存空间
- 进行内存区域的申请时,需要注意避免发生一下错误:
- 内存分配未成功,却使用了它
- 内存分配虽然成功,但是尚未初始化就引用它
- 内存分配成功且初始化,但操作越过了内存边界
- 忘记释放内存,造成内存泄漏
- 释放了内存却继续使用它。
一个程序的内存划分:代码区、常量区、全局数据区、堆区、栈区
其它的内存操作函数:
memset:
- 功能:内存初始化函数
- 函数原型: void *memset(起始地址,要设置的值,要设置多大区域)
- 一般用于给刚申请的内存设置一个初值
- 返回值:指向起始地址的指针
memcpy:
- 功能:内存拷贝函数
- 函数原型:void *memcpy(目标地址,源数据地址,要拷贝多大区域);
memmove:
- 功能:内存移动
- 函数原型:memmove(要移动的目标位置,源位置,移动的字节数);
七、文件处理
“文件”是指存储在计算机外部存储器中的数据的集合。C语言将文件看作是字符构成的序列,即字符流,其基本的存储单位是字节。
C语言文件的打开模式:
打开模式 | 简述 | 若欲操作的 文件不存在 | 成功打开文件后 文件指针位置 | 是否清空 原有内容 | 读取位置 |
---|---|---|---|---|---|
r | 只读 | 打开失败 | 开头 | 否 | 任意位置读取 |
w | 只写 | 新建 | 开头 | 是 | 不可读取 |
a | 新建 | 结尾 | 否 | 不可读取 | |
r+ | 读写 | 打开失败 | 开头 | 否 | 任意位置读取 |
w+ | 新建 | 开头 | 是 | 任意位置读取 | |
a+ | 新建 | 结尾 | 否 | 任意位置读取 |
文件读写操作函数:
函数名 | 功能 |
---|---|
fputc/fgetc | 逐字符读写 |
fputs/fgets | 逐行读写 |
fprintf/fscanf_s | 格式化读写 |
fread/fwrite | 按数据块读写的函数 |
feof() | 判断文件是否到结尾 |
rewind() | 将指向文件的指针重新指向文件开始位置 |
fseek() | 使指向文件的指针指向文件任何一个位置,实现随机读写文件 |
ftell() | 用于测试指向文件的指针的当前位置 |
char cCh = fgetc(fpFile);//返回文件当前位置字符,并使文件位置指针指向下一个字符,遇到文件结束,返回文件结束标志EOF
fputc(字符,文件指针);//将字符写入文件当前位置,指针下移,写入成功,返回值是该字符,否则返回EOF
fputs(字符串,文件指针);//写入成功返回0,否则EOF,注意:'\0'不写入
fgets(字符串数组,数组大小,文件指针);//返回值;char *型,指向读入的字符串内容,输入失败,返回NULL
fprintf(文件指针,格式控制字符串,输出列表);
fscanf(文件指针,格式控制,输入列表);
fread ( void *buffer, size_t size, size_t count, FILE *stream) ;
fwrite(存放地址, 大小, 数据块个数, 文件指针);//操作成功返回实际写入文件的数据块个数
feof(文件指针);//若文件当前位置为结尾,返回非零值,否则返回0
rewind(文件指针);//无返回值
fseek(文件指针, 偏移量, 起始位置);//说明:函数fseek将以文件的起始位置为基准,根据偏移量往前或往后移动指针。其中偏移量是一个长整型数,表示从起始位置移动的字节数,正数表示指针往后移。起始位置用宏SEEK_SET、SEEK_CUR、SEEK_END代表文件开始、文件当前位置和文件结束位置。指针设置成功,返回0.否则非零。
int nOffset = ftell(fpFile);//返回值:长整型数,测试成功,返回文件指针当前位置距文件开头的字节数,否则返回-1L。
八、项目的文件组织
为什么需要管理组织源文件?
当我们的代码越来越复杂之后,就需要将不同功能的代码分别放到多个源文件中,
C语言项目是由一个以上的源文件和多个头文件组成,一个项目只能有一个main函数,C项目生成的目标文件是由多个源文件生成的代码,’.c’文件一般会有一个与之对应的’.h’文件。它们文件名一样,后缀不一样。
源文件的组织原则
- 在头文件中约定只能存放声明性的东西
- 宏的声明、函数头声明、全局变量的声明、结构体联合体的声明
- 如果有多个源文件包含了一个头文件,这就相当于将这个头文件内容复制在多个源文件中,这样会导致头文件的内容存在多分,如果这些内容包含了变量的定义,函数的定义,就会导致重定义错误的发生
- 注意:
- 只有模块自己使用的函数、数据,不要用extern在头文件声明
- 只有模块自己使用的宏、常量、类型也不要在头文件声明,应该在自己的".c"文件里声明,防止重复包含
- 使用宏"#pragma once"防止一个头文件被重复包含
- 不要包含只有在本模块才使用的头文件,这些头文件应该在“.c”文件中包含
- 接口文件要有面向用户的充足的注释,接口文件在发布后要尽量避免修改,即使修改也要保证不影响用户程序
- 在源文件中保存定义性质的的东西
- 函数定义、全局变量定义
- 一个源文件中所存放内容(函数定义),他们的性质应该是相同的,例如一个存放字符串操作的源文件中,就不应该有除字符串操作函数之外的函数了
- 源文件组织的基本建议
- 用层次化和模块化的软件开发模型,每一个模块只能使用所在层和下一层模块提供的接口,每个模块的文件保存在独立的一个文件夹中。
- 通常情况下,实现一个模块的文件不止一个,这些相关的文件应该保存在一个文件夹中。
- 声明和定义分开,使用“.h”文件暴露模块需要提供给外部的函数、宏、类型、常量、全局变量,尽量做到模块对外部透明。
- 文件夹和文件命名要能反映出模块的功能。