C语言学习笔记(二)

一、函数

函数的基本概念

  1. 有特定功能的一小段程序
  2. C语言基本构成单位

函数的分类

  1. 标准C定义的标准库函数
  2. 第三方库函数
  3. 自定义函数

函数的定义
定义格式:返回值类型 函数名(参数类型 参数名……)
说明:定义函数主要考虑三个问题:

  1. 函数的主要功能
  2. 需要的外部参数
  3. 返回结果

函数的调用

  1. 使用条件

    1. 定义在调用者函数之前,可直接调用
    2. 定义在调用者函数之后,需要先声明函数声明
    3. 声明时,可以没有变量的名字,但必须要有类型 int FUN(int ,int )
  2. 注意事项(在一个函数中调用另一个函数需具备以下条件)

    1. 被调用函数存在
    2. 使用库函数,需添加头文件 #include <xxx.h>
    3. 使用自定义函数,而该函数在主调函数之后(在同一文件),需声明
    4. 即使没有实现函数,但有声明,在写的时候没有错误,但在编译链接时会出错(无法解析XXX外部符号)
    5. 将函数作为另一个函数的参数时,该函数返回值类型与当前函数参数类型一致
  3. 函数语句

    把函数调用作为一个语句。这时不需要函数带返回值,只要求完成一定操作

  4. 函数表达式

    函数出现在一个表达式中,此时需要函数返回一个确定的值参加运算

  5. 函数参数

    函数调用作为一个函数的实参

二维数组作为函数参数的正确写法:

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是实参
}

说明:

  1. 实参可以是常量、变量或者表达式,但要求他们有确定的值
  2. 实参和形参的类型必须一致
  3. 单向传递,只能由实参传递给形参,而不能形参传递给实参
  4. 形参不能改变实参的值

二、作用域

在C语言中,作用域分为三种:文件作用域、函数作用域、复合语句作用域
变量类型分为:局部变量、全局变量、静态局部变量
局部变量

  1. 定义在函数内部,只对当前函数有效
  2. 如果{}里面定义的局部变量与外面的重名,则以内部为准

全局变量

  1. 定义在函数外部,对所有函数有效
  2. 作用范围:可以被在其定义位置之后的所有函数所共享

静态局部变量

  1. 所有的全局变量均为静态变量
  2. 局部静态变量需加修饰符stasic
  3. 静态变量的特点:在程序的整个执行过程中始终存在,但是在他作用域外不能使用。静态变量的生存期就是整个程序的运行期,函数体内如果在定义静态比变量的同时初始化,则以后程序不再进行初始化操作。
#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 在全局变量中的应用:

  1. 如果组成一个程序的几个文件需要用到同一个全局变量,只要在其它引用该全局变量的源程序文件中说明该全局变量为extern即可
  2. 如果一个源程序文件中的全局变量仅限于该文件使用,只要在该全局变量定义时说明加static即可

三、预处理

编译预处理:主要用于在程序正式编译前进行一些预加工操作,所有的编译预处理命令均以#开头,编译预处理共分为几个方面:

  1. 宏定义
  2. 文件包含
  3. 条件编译

:宏定义的作用就是在程序中某段代码的一个别名,主要是为程序调试、移植等提供便利,所有的宏命令都以符号#define开头,并且结尾不用分号。

#define PI 3.14

#undef PI

宏定义的作用就是在编译预处理时,将源程序中所有标识符替换成语句序列

  1. 宏名一般大写
  2. 有效范围:从定义位置到文件结束。如需要终止宏定义作用域,可以用#undef命令

无参宏:指执行单一替换功能的宏定义
注意事项:

  1. 宏定义时可以引用已经定义的宏名
  2. 对程序中用双引号括起来的字符串内的字符,不进行宏的替换操作

有参宏:可以让我们在定义宏时带参数扩大宏的应用范围。
格式:#define 标识符(参数表) 字符串
作用:在编译预处理时,将源程序中所有标识符替换成字符串,并且将字符串中的参数用实际使用的参数替换
注意:

  1. 宏名和参数之间无空格
#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");
}

文件包含

  1. 标准方式 #include <xxxx.x>
    只按照标准C方式在C语言编译器的C函数库头文件中查找要包含的文件
  2. 通用方式 #include “xxxx.x”
    先在源文件所在的目录中查找要包含的文件,若未找到,按标准方式查找

条件编译

四、指针

定义变量的本质

  1. 开辟一块内存空间,并用变量名代表那块内存空间
  2. 内存空间的最小单位是字节。每一个字节都有一个编号,这个编号我们称为地址。
  3. 有一种特殊的变量,专门存储地址,这种变量叫做指针变量。指针变量也有一块内存空间,保存的是指向目标变量的地址。
    在这里插入图片描述

指针变量的基本使用方式

  1. 定义指针变量
  2. 给指针变量赋值
  3. 指针解引用

在这个过程中需要使用两个运算符:取地址运算符&和解引用运算符*

定义指针变量:

定义格式:类型名 *指针变量名

int *pNums;

注意:

  1. 变量名前*是一个说明符,用来说明该变量是指针变量,这是*不能省略,但是他不是变量名的一部分
  2. 类型名表示指针变量所指向的变量的类型,而且只能指向这种类型的变量

给指针变量赋值:

int a = 12;
int *p = 0;
p = &a;			//&a表示a的地址
int temp = *p;  //等价于 temp =a;
*p = 200;       //等价于 a=200;*p 表示指针变量p指向的变量

注意事项:

  1. 指针变量是用来存放地址的,不要给指针变量赋常数值,如 int *p = 1000;
  2. 指针变量没有指向明确的地址前,不要对它所指的对象赋值,如 int b = 5;*p = b;//类型不匹配
  3. 指针变量不能修改常量

指针解引用

我们也知道,指针变量所对应的空间保存的是一个地址,我们要使用这个指针去修改指向变量的值,就需要用到解引用运算符*

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对参数的要求。

指针与函数传参

指针是间接引用,利用这一特性进行参数传递,使得函数内值的变化可以影响函数外部,指针传参的本质是地址传递。

指针的运算

指针只能进行加法和减法运算:+ - ++ – += -=并且只要两种形式

  1. 指针±整数:指针加或减一个整数,得到的数据类型还是指针
  2. 指针-指针:得到的是这两个地址间能够存放多少个这种类型的数据

指针和二维数组
二维数组名也是一个指针类型,叫做一维数组指针类型(数组名是数组类型的常量)
定义:类型名 (*变量名)[数组长度]
数组指针应该给出所指向数组的长度

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级指针
  2. 、对一维数组指针使用下标运算符也会降维,变为1级指针
  3. 、对于一维数组指针+1,会得到下一排起始地址,类型还是数组指针
  4. 、对一维数组名取地址,会变为二维数组指针,数值不变
实参所匹配的形参
数组的数组char c[8][10];char (*c)[10];数组指针
指针数组char *c[10];char **c;指针的指针
数组指针(行指针)char (*c)[10];char (*c)[10];不改变
指针的指针char **c;char **c;不改变

指针数组和数组指针
指针数组:

  1. 本质是数组,里面元素为指针
  2. 数据类型 *指针数组名[常量表达式]
  3. 如:int *p[6]

数组指针:

  1. 本质是指针,指向一个数组
  2. 类型名 (*变量名)[数组长度]
  3. 如:int (*p)[6]

判断规则:

  1. 有*和[],[]优先级最高
  2. 变量名先和谁结合就是什么类型

区别:

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;
};

结构体和联合体的区别:

  1. 结构体,每一个成员单独占用内存空间
  2. 联合体,所有的成员共享内存空间

六、堆内存

内存空间的动态分配:

  1. 定义一个指针变量
  2. 申请一片内存空间,并将首地址赋值给指针变量,此时便可通过指针变量访问这片内存
  3. 用完后释放这片内存空间

涉及函数:malloc和free
malloc:

  1. 功能:申请空间
  2. 参数:申请内存大小,字节数
  3. 返回值,申请出的空间的起始地址,类型void*
  4. 堆空间:内存数据是随机(debug版本默认以4个fd开头结尾,其他用cd来填充)

free:

  1. 功能:释放空间
  2. 只能释放malloc开辟出来的空间
  3. 不能重复释放相同地址
  4. 释放后记得将指针置为NULL,防止指针悬空
int *p = NULL;
p = (int *malloc(sizeof(int));	//申请内存
free(p);	//释放内存

使用堆和使用数组的区别

  1. 数组长度是常量,堆大小可以是变量
  2. 堆需要自己释放,不释放或忘记释放,会造成内存泄漏。数组不需要自己释放

悬空指针:释放内存后,指针应及时的赋值为NULL。不赋值为NULL的话,称为悬空指针
和野指针:指针变量被定义后,没有被初始化,这种指针被称为野指针
指针总结:指针变量,要么指向有意义的地方(变量,数组,堆),要么就指向NULL(0)。

注意事项

  1. 刚刚分配的动态内存的初始值是不确定的
  2. 不能对同一指针(地址)连续两次进行free操作
  3. 不能对指向静态内存区(全局变量)或栈内存区(局部变量)的指针应用free(但可以对空指针NULL应用free),对一个指针应用free后,它的值不会改变,但它指向了一个无效的内存区,这个是悬空指针
  4. 如果没有及时释放某块动态内存,并且将指向它的指针指向了别处,就会造成“内存泄漏”,执行malloc和free函数有一定的代价,所以对于较小的数据量不应该在动态内存之中,并且尽量避免频繁的分配和释放内存空间
  5. 进行内存区域的申请时,需要注意避免发生一下错误:
    1. 内存分配未成功,却使用了它
    2. 内存分配虽然成功,但是尚未初始化就引用它
    3. 内存分配成功且初始化,但操作越过了内存边界
    4. 忘记释放内存,造成内存泄漏
    5. 释放了内存却继续使用它。

一个程序的内存划分:代码区、常量区、全局数据区、堆区、栈区
在这里插入图片描述
其它的内存操作函数
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’文件。它们文件名一样,后缀不一样。
源文件的组织原则

  • 在头文件中约定只能存放声明性的东西
  1. 宏的声明、函数头声明、全局变量的声明、结构体联合体的声明
  2. 如果有多个源文件包含了一个头文件,这就相当于将这个头文件内容复制在多个源文件中,这样会导致头文件的内容存在多分,如果这些内容包含了变量的定义,函数的定义,就会导致重定义错误的发生
  3. 注意:
    1. 只有模块自己使用的函数、数据,不要用extern在头文件声明
    2. 只有模块自己使用的宏、常量、类型也不要在头文件声明,应该在自己的".c"文件里声明,防止重复包含
    3. 使用宏"#pragma once"防止一个头文件被重复包含
    4. 不要包含只有在本模块才使用的头文件,这些头文件应该在“.c”文件中包含
    5. 接口文件要有面向用户的充足的注释,接口文件在发布后要尽量避免修改,即使修改也要保证不影响用户程序
  • 在源文件中保存定义性质的的东西
  1. 函数定义、全局变量定义
  • 一个源文件中所存放内容(函数定义),他们的性质应该是相同的,例如一个存放字符串操作的源文件中,就不应该有除字符串操作函数之外的函数了
  • 源文件组织的基本建议
  1. 用层次化和模块化的软件开发模型,每一个模块只能使用所在层和下一层模块提供的接口,每个模块的文件保存在独立的一个文件夹中。
  2. 通常情况下,实现一个模块的文件不止一个,这些相关的文件应该保存在一个文件夹中。
  3. 声明和定义分开,使用“.h”文件暴露模块需要提供给外部的函数、宏、类型、常量、全局变量,尽量做到模块对外部透明。
  4. 文件夹和文件命名要能反映出模块的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值