C 语言基础(二)
6函数
6.1 概述
6.1.1 函数分类
C 程序是有函数组成的, 代码都是由主函数 main() 开始执行的. 函数是 C 程序的基本模块, 适用于完成特定任务的程序代码单元.
从函数定义的角度看, 函数可分为系统函数和用户定义函数两种
- 系统函数, 即库函数:这是由编译系统提供的, 用户不必自己定义这些函数,可以直接使用他们, 如常用的打印函数printf().
- 用户定义函数:用以解决用户的专门需要.
6.1.2 函数的作用
- 函数使用可以省去重复代码的编写, 降低代码重复率.
- 函数可以让程序更加模块化, 从而有利于程序的阅读,修改和完善.
6.2 函数的定义
6.2.1 函数定义格式
函数定义的一般形式:
返回类型 函数名(形式参数列表)
{
数据定义部分;
执行语句部分;
}
int max(int a, int b)
{
int c = 0;
if(a > b){
c = a;
}else{
c = b;
}
return c;
}
/*
函数名: 用户定义的标识符,类似于给一个变量起名字
返回类型: 如有返回值, 函数体里通过return返回, return后面的变量尽量和返回类型匹配,如代码中的 int
形参列表: 可以是给类型的变量, 各参数之间用逗号间隔
函数体:大括号{}内的内容即为函数体内容,一般有数据定义,语句执行,return返回值
*/
6.2.2 函数名 形参 函数体 返回值
-
函数名
理论上是可以随意起名字,最好起的名字见名知意,应该让用户看到这个函数名就知道这个函数的功能. 注意: 函数名后面有个圆括号(), 代表这个味函数, 不是普通的变量名.
-
形参列表
在定义函数时指定的形参, 在未调用函数时,它们并不占内存中的存储单元, 因此称他们是形式参数或虚拟参数, 简称形参, 表示并不时实际存在的数据, 所以形参里变量不能赋值
在定义函数时指定的形参,必须是 类型+变量 的形式:
//1: right, 类型+变量 void max(int a, int b) { } //2: error, 只有类型,没有变量 void max(int, int) { } //3: error, 只有变量,没有类型 int a, int b; void max(a, b) { }
在定义函数时指定的形参和返回值根据函数的需要来设计,如果没有形参,圆括号内容为空,或写一个 void 关键字
-
花括号{ } 里的内容即为函数体的内容, 这里为函数功能实现的过程,这和之前的代码没太大区别, 之前我们把代码写在 main ()函数里,现在只是把这些写到别的函数里.
-
返回值
函数的返回值是通过函数中的 return 语句获得的, return 后面的值也可以是一个表达式
-
尽量保证 return 语句中表达式的值和函数返回类型是同一类型.
int max() // 函数的返回值为int类型 { int a = 10; return a;// 返回值a为int类型,函数返回类型也是int,匹配 }
-
如果函数返回的类型和 return 语句中表达式的值不一样,则以函数返回类型为准, 即**函数返回类型决定返回值的类型.**返回值类型可以自动进行类型转换.
double max() // 函数的返回值为double类型 { int a = 10; return a;// 返回值a为int类型,它会转为double类型再返回 }
注意: 如果函数返回的类型和 return 语句中表达式的值不一致,而他又无法自动进行类型转换, 程序则会报错.
-
return 语句的另一个作用为中断 return 所在的执行函数, 类似于 break 终端循环和 switch 语句一样.
int max() { return 1;// 执行到,函数已经被中断,所以下面的return 2无法被执行到 return 2;// 没有执行 }
-
如果函数带返回值, return 后面必须跟着一个值, 如果函数没有返回值, 函数名字的前面必须写一个 void 关键字, 这时候, 我们写代码时也可以通过 return 中断函数, 只是这时, return 后面不带内容(分号除外)
6.3 函数的调用
函数后, 我们需要调用此函数才能执行到这个函数里的代码. 这和 main函数不一样, main () 为编译器设定好自动调用的主函数, 无需人为调用, 我们都是在 main() 函数里面调用别的函数, **一个 C 程序里有且只有一个 main() 函数.
6.3.1 函数执行流程
#include <stdio.h>
void print_test()
{
printf("this is for test\n");
}
int main()
{
print_test(); // print_test函数的调用
return 0:
}
- 进入main()函数
- 调用print_test()函数:
- 它会在main()函数的前寻找有没有一个名字叫“print_test”的函数定义;
- 如果找到, 接着检查函数的参数, 这里调用函数时没有传参, 函数定义也没有形参, 参数类型匹配;
- 开始执行print_test()函数,这时候,main()函数里面的执行会阻塞( 停 )在print_test()这一行代码,等待print_test()函数的执行。
- print_test()函数执行完( 这里打印一句话 ),main()才会继续往下执行,执行到return 0, 程序执行完毕。
6.3.2 函数的形参和实参
- 形参出现在函数定义中, 在整个函数日内部都可以使用, 离开该函数则不能使用.
- 是惨出现在主调函数中,进入被调函数后, 实参也不能使用.
- 实参变脸对形参变量的数据传递是 “值传递”,即单向传递,只有实参传递给形参,而不能由形参传递回来给实参.
- 在调用函数时,编译系统临时给形参分配存储单元. 调用结束后, 形参单元被释放.
- 实参单元与星灿但愿是不同的单元. 调用结束后, 形参单元被释放, 函数调用结束返回主调函数后则不能在使用该形参变量. 实参单元仍保留并维持原值. 因此,在执行一个被调用函数时, 形参的值如果发生改变,并不会改变主调函数中实参的值.
6.3.3 无参函数调用
如果是调用无参函数, 则不能加上 “实参” , 但括号不能省略
// 函数的定义
void test()
{
}
int main()
{
// 函数的调用
test(); // right, 圆括号()不能省略
test(250); // error, 函数定义时没有参数
return 0;
}
6.3.4 有参函数调用
-
如果是参列表包含多个实参, 则各参数间用逗号隔开.
// 函数的定义 void test(int a, int b) { } int main() { int p = 10, q = 20; test(p, q); // 函数的调用 return 0; }
-
实参与形参的个数应相等, 类型应匹配. 实参与形参按顺序对应, 一对一地传递数据.
-
实参可以是常量,变量,或表达式, **无论是餐是何种类型的量, 在进行函数调用时, 他们都必须具有确定的值, 以便把这些值传送给形参. **所以, 这里的变脸是在圆括号 ( ) 外面定义好, 赋好值的变量.
// 函数的定义 void test(int a, int b) { } int main() { // 函数的调用 int p = 10, q = 20; test(p, q); // right test(11, 30 - 10); // right test(int a, int b); // error, 不应该在圆括号里定义变量 return 0; }
6.3.5 函数返回值
-
如果函数定义没有返回值, 函数调用时不能写 void 关键字, 调用函数时也不能接收函数的返回值.
// 函数的定义 void test() { } int main() { // 函数的调用 test(); // right void test(); // error, void关键字只能出现在定义,不可能出现在调用的地方 int a = test(); // error, 函数定义根本就没有返回值 return 0; }
-
如果函数定义有返回值,这个返回值我们根据用户需要可用可不用, 假如需要使用函数返回值, 我们需要定义一个匹配类型的变量来接受
// 函数的定义, 返回值为int类型 int test() { } int main() { // 函数的调用 int a = test(); // right, a为int类型 int b; b = test(); // right, 和上面等级 char *p = test(); // 虽然调用成功没有意义, p为char *, 函数返回值为int, 类型不匹配 // error, 必须定义一个匹配类型的变量来接收返回值 // int只是类型,没有定义变量 int = test(); // error, 必须定义一个匹配类型的变量来接收返回值 // int只是类型,没有定义变量 int test(); return 0; }
6.4 函数的声明
如果使用用户自己定义的函数, 而该函数与调用他的函数(主调函数) 不在同一文件中, 或者**函数定义的位置在主调函数之后, 则必须在调用此函数之前对被调的函数作声明.
所谓函数声明, 就是在函数尚未定义的情况下, 实现将该函数的有关信息功过编译系统, 相当于告知编译器, 函数在后面定义, 以便使编译能正常进行.
**注意:**一个函数只能被定义一次, 但可以声明多次.
#include <stdio.h>
int max(int x, int y); // 函数的声明,分号不能省略
// int max(int, int); // 另一种方式
int main()
{
int a = 10, b = 25, num_max = 0;
num_max = max(a, b); // 函数的调用
printf("num_max = %d\n", num_max);
return 0;
}
// 函数的定义
int max(int x, int y)
{
return x > y ? x : y;
}
函数定义和声明的区别:
- 定义是指对函数功能的确定, 包括指定函数名, 函数类型, 形参及其类型, 函数体等, 他是一个完整的, 独立的函数单位.
- 生命的作用则是把韩叔的名字, 函数类型以及形参的个数,类型和顺序通知编译系统, 以便在对包含函数调用的语句进行编译时,据此对其进行对照检查 (例如函数名是否正确, 实参与形参的类型和个数是否一致).
6.5 main 函数与 exit 函数
在 main 函数中调用 exit 和 return 结果是一样的, 但在职函数中调用 return 只是代表子函数终止了, 在子函数中调用 exit ,那么程序终止.
#include <stdio.h>
#include <stdlib.h>
void fun()
{
printf("fun\n");
//return;
exit(0);
}
int main()
{
fun();
while (1);
return 0;
}
6.6 多文件(份文件) 编程
6.6.1 份文件编程
- 把函数声明放在头文件 xxx.h 中, 在主函数中包含相应头文件
- 在头文件对应的 xxx.c 中实现 xxx.h 声明函数
main.c
#include <stdio.h>
#include <fun.h>
int main(void)
{
int a = 10, b = 20, max_num, min_num;
max_num=max(a,b);
min_num=min(a,b);
printf(.......)
}
fun.c
int max(int x, int y)
{
return x>y?x:y;
}
int min(int x, int y)
{
return x<y?x:y;
}
func.h
extern int max(int x, int y);
extern int max(int x, int y);
6.6.2 防止头文件重负包含
但一个项目比较大时, 往往都是分文件, 这时候又可能不小心把同一个头文件 include 多次, 或者头文件嵌套包含.为了避免头一个文件被 include 多次, C/C++中有两种方式,一种是 #ifdef 方式, 一种是 #pragma once 方式.
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
// 声明语句
#endif
#pragma once
// 声明语句
7 指针
7.1 概述
7.1.1 内存
内存含义:
- 储存器: 计算机的组成中, 用来储存程序和数据, 辅助 cpu 进行运算处理的重要部分.
- 内存: 内存储存器, 暂存程序/数据------掉电丢失 SRAM, DRAM, DDR, DDR2, DDR3
- 外存:外部存储器, 长时间保存程序/数据------掉电不丢
内存是沟通 CPU 与硬盘的桥梁:
- 暂存放 CPU 中的运算数据
- 暂存与硬盘等外部存储器交换的数据
7.1.2 物理存储器和存储地址空间
有关内存的两个概念: 物理存储器和存储地址空间.
物理存储器:实际存在的具体存储器芯片.
- 主板上安插的内存
- 显示卡上显示 RAM 芯片
- 各种适配卡上的 RAM 芯片和 ROM 芯片
储存地址空间: 对储存器编码的范围. 我们在软件上常说的内存是指这一层含义.
- 编码: 对每个物理存储单元 (一个字节) 分配一个号码
- 寻址: 可以根据分配的号码找到相应的存储单元, 完成数据的读写
7.1.3 内存地址
- 将内存抽象成一个很大的一维字符数组.
- 编码就是对内存的每一个字节分配一个32位或64位的编号 (与处理器相关)
- 这个内存编号我们称之为内存地址.
内存中的每一个数据都会分配相应的地址:
- char : 占一个字节分配一个地址
- int : 占四个字节分配四个地址
7.1.4 指针和指针变量
- **内存区的每一个字节都有一个编号, 这就是 “地址” **
- 如果在程序中定义了一个变量, 在对程序进行编译或者运行时, 系统就会给这个变量分配内存单元, 并确定他的内存地址编号
- 指针的实质就是内存 “地址” . 指针变量是存放地址的变量.
- 通常我们叙述时会把指针变量简称指针,实际它们含义并不一样
7.2 指针基础知识
7.2.1 指针变量的定义和使用
- 指针也是一种数据类型, 指针变量也是一种变量
- 指针变量指向谁, 就把谁的地址复制给指针变量
- " * "操作符操作的是指针变量指向的内存空间
#include <stdio.h>
int main()
{
int a = 0;
char b = 100;
printf("%p, %p\n", &a, &b); //打印a, b的地址
//int *代表是一种数据类型,int*指针类型,p才是变量名
//定义了一个指针类型的变量,可以指向一个int类型变量的地址
int *p;
p = &a;//将a的地址赋值给变量p,p也是一个变量,值是一个内存地址编号
printf("%d\n", *p);//p指向了a的地址,*p就是a的值
char *p1 = &b;
printf("%c\n", *p1);//*p1指向了b的地址,*p1就是b的值
return 0;
}
注意 :& 可以去的一个变量在内存中的地址. 但是, 不能去寄存器变量, 因为寄存器变量不在内存里, 而在 CPU 里面, 所以是没有地址的.
7.2.3 指针大小
- 使用 sizeof() 测量指针的大小, 得到的总是: 4 或 8 (32位系统, 64位系统)
- sizeof () 测量的是指针变量指向存储地址的大小
int *p1;
int **p2;
char *p3;
char **p4;
printf("sizeof(p1) = %d\n", sizeof(p1));
printf("sizeof(p2) = %d\n", sizeof(p2));
printf("sizeof(p3) = %d\n", sizeof(p3));
printf("sizeof(p4) = %d\n", sizeof(p4));
printf("sizeof(double *) = %d\n", sizeof(double *));
注意: 定义指针的类型与所指向地址存储的数据类型相一致, 此数据类型告诉指针所指向空间的大小.
7.2.4 野指针和空指针
指针变量也是变量, 是变量就可以任意赋值, 不要越界即可(32 位为 4 字节, 64 位为 8 字节), 但是, 任意数值赋值给指针变量没有意义, 因为这样的指针就成了野指针, 此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域). 所以, 野指针不会直接引发错误, 操作也指针指向的内存区域才会出问题
int a = 100;
int *p;
p = a; //把a的值赋值给指针变量p,p为野指针, ok,不会有问题,但没有意义
p = 0x12345678; //给指针变量p赋值,p为野指针, ok,不会有问题,但没有意义
*p = 1000; //操作野指针指向未知区域,内存出问题,err
但是, 野指针和有效指针变量保存的都是数值, 为了标志此指针变量没有指向任何变量 (空闲可以用) , C 语言中, 可以吧 NULL 赋值给此指针, 这样就标志此指针为空指针, 没有任何指向. (NULL 是一个值为 0 的宏常量)
7.2.5 万能指针 void *
void * 指针可以指向任意变量的内存空间:
void *p = NULL;
int a = 10;
p = (void *)&a; //指向变量时,最好转换为void *
//使用指针变量指向的内存时,转换为int *
*( (int *)p ) = 11;
printf("a = %d\n", a);
7.2.6 const 修饰的指针变量
int a = 100;
int b = 200;
//指向常量的指针
//修饰*,指针指向内存区域不能修改,指针指向可以变
const int *p1 = &a; //等价于int const *p1 = &a;
//*p1 = 111; //err
p1 = &b; //ok
//指针常量
//修饰p1,指针指向不能变,指针指向的内存可以修改
int * const p2 = &a;
//p2 = &b; //err
*p2 = 222; //ok
7.3 指针和数组
7.3.1 数组名
数组名字是数组的首元素地址, 但是他是一个常量:
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
printf("a = %p\n", a);
printf("&a[0] = %p\n", &a[0]);
//a = 10; //err, 数组名只是常量,不能修改
7.3.2 指针操作数组元素
#include <stdio.h>
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
int n = sizeof(a) / sizeof(a[0]);
for (i = 0; i < n; i++)
{
//printf("%d, ", a[i]);
printf("%d, ", *(a+i));
}
printf("\n");
int *p = a; //定义一个指针变量保存a的地址
for (i = 0; i < n; i++)
{
p[i] = 2 * i;
}
for (i = 0; i < n; i++)
{
printf("%d, ", *(p + i));
}
printf("\n");
return 0;
}
7.3.3 指针加减运算
- 加法运算
- 指针计算不是简单的整数相加
- 如果是一个 int * , +1 的结果是增加一个 int 的大小
- 如果是一个 char * , +1 的结果是增加一个 char 的大小
#include <stdio.h>
int main()
{
int a;
int *p = &a;
printf("%d\n", p);
p += 2;//移动了2个int
printf("%d\n", p);
char b = 0;
char *p1 = &b;
printf("%d\n", p1);
p1 += 2;//移动了2个char
printf("%d\n", p1);
return 0;
}
通过改变指针指向操作数组元素:
#include <stdio.h>
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
int n = sizeof(a) / sizeof(a[0]);
int *p = a;
for (i = 0; i < n; i++)
{
printf("%d, ", *p);
p++;
}
printf("\n");
return 0;
}
-
减法运算
示例1:
#include <stdio.h> int main() { int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int i = 0; int n = sizeof(a) / sizeof(a[0]); int *p = a+n-1; for (i = 0; i < n; i++) { printf("%d, ", *p); p--; } printf("\n"); return 0; }
示例2:
#include <stdio.h> int main() { int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int *p2 = &a[2]; //第2个元素地址 int *p1 = &a[1]; //第1个元素地址 printf("p1 = %p, p2 = %p\n", p1, p2); int n1 = p2 - p1; //n1 = 1 int n2 = (int)p2 - (int)p1; //n2 = 4 printf("n1 = %d, n2 = %d\n", n1, n2); return 0; }
7.3.4 指针数组
指针数组,他是数组,数组的每个元素都是指针类型
#include <stdio.h>
int main()
{
//指针数组
int *p[3];
int a = 1;
int b = 2;
int c = 3;
int i = 0;
p[0] = &a;
p[1] = &b;
p[2] = &c;
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++ )
{
printf("%d, ", *(p[i]));
}
printf("\n");
return 0;
}
7.4 多级指针
- C 语言允许有多级指针存在, 在实际的程序中以及指针最常用, 其次是二级指针.
- 二级指针就是指向一个一级指针变量地址的指针.
- 三级指针基本用不到 (除了考试)
int a = 10;
int *p = &a; //一级指针
*p = 100; //*p就是a
int **q = &p;
//*q就是p
//**q就是a
int ***t = &q;
//*t就是q
//**t就是p
//***t就是a
7.5 指针和函数
7.5.1 函数形参改变实参的值
#include <stdio.h>
void swap1(int x, int y)
{
int tmp;
tmp = x;
x = y;
y = tmp;
printf("x = %d, y = %d\n", x, y);
}
void swap2(int *x, int *y)
{
int tmp;
tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 3;
int b = 5;
swap1(a, b); //值传递
printf("a = %d, b = %d\n", a, b);
a = 3;
b = 5;
swap2(&a, &b); //地址传递
printf("a2 = %d, b2 = %d\n", a, b);
return 0;
}
7.5.2 数组名做函数参数
数组名做函数参数, 函数的形参会退化为指针:
#include <stdio.h>
//void printArrary(int a[10], int n)
//void printArrary(int a[], int n)
void printArrary(int *a, int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
printf("%d, ", a[i]);
}
printf("\n");
}
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int n = sizeof(a) / sizeof(a[0]);
//数组名做函数参数
printArrary(a, n);
return 0;
}
7.5.3 指针作为函数的返回值
#include <stdio.h>
int a = 10;
int *getA()
{
return &a;
}
int main()
{
*( getA() ) = 111;
printf("a = %d\n", a);
return 0;
}
7.6 指针和字符串
7.6.1 字符指针
#include <stdio.h>
int main()
{
char str[] = "hello world";
char *p = str;
*p = 'm';
p++;
*p = 'i';
printf("%s\n", str);
p = "mike jiang";
printf("%s\n", p);
char *q = "test";
printf("%s\n", q);
return 0;
}
7.6.2 字符指针做函数参数
#include <stdio.h>
void mystrcat(char *dest, const char *src)
{
int len1 = 0;
int len2 = 0;
while (dest[len1])
{
len1++;
}
while (src[len2])
{
len2++;
}
int i;
for (i = 0; i < len2; i++)
{
dest[len1 + i] = src[i];
}
}
int main()
{
char dst[100] = "hello mike";
char src[] = "123456";
mystrcat(dst, src);
printf("dst = %s\n", dst);
return 0;
}
7.6.3 const 修饰的指针变量
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
//const修饰一个变量为只读
const int a = 10;
//a = 100; //err
//指针变量, 指针指向的内存, 2个不同概念
char buf[] = "aklgjdlsgjlkds";
//从左往右看,跳过类型,看修饰哪个字符
//如果是*, 说明指针指向的内存不能改变
//如果是指针变量,说明指针的指向不能改变,指针的值不能修改
const char *p = buf;
// 等价于上面 char const *p1 = buf;
//p[1] = '2'; //err
p = "agdlsjaglkdsajgl"; //ok
char * const p2 = buf;
p2[1] = '3';
//p2 = "salkjgldsjaglk"; //err
//p3为只读,指向不能变,指向的内存也不能变
const char * const p3 = buf;
return 0;
}
7.6.4 指针数组做为 main 函数的形参
int main(int argc, char *argv[]);
- main 函数是操作系统调用的, 第一个参数标明 argv 数组的成员数量, argv数组的每个成员都是 char * 类型
- argv 是命令行参数的字符串数组
- argc 代表命令行参数的数量, 程序名字本身算第一个参数
#include <stdio.h>
//argc: 传参数的个数(包含可执行程序)
//argv:指针数组,指向输入的参数
int main(int argc, char *argv[])
{
//指针数组,它是数组,每个元素都是指针
char *a[] = { "aaaaaaa", "bbbbbbbbbb", "ccccccc" };
int i = 0;
printf("argc = %d\n", argc);
for (i = 0; i < argc; i++)
{
printf("%s\n", argv[i]);
}
return 0;
}
7.8 指针小结
定义 | 说明 |
---|---|
int i | 定义整形变量 |
int *p | 定义一个指向int的指针变量 |
int a[10] | 定义一个有10个元素的数组,每个元素类型为int |
int *p[10] | 定义一个有10个元素的数组,每个元素类型为int* |
int func() | 定义一个函数,返回值为int型 |
int *func() | 定义一个函数,返回值为int *型 |
int **p | 定义一个指向int的指针的指针,二级指针 |