C ++ 学习之 C 语言基础(二) 函数 指针

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. 函数名

    理论上是可以随意起名字,最好起的名字见名知意,应该让用户看到这个函数名就知道这个函数的功能. 注意: 函数名后面有个圆括号(), 代表这个味函数, 不是普通的变量名.

  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 关键字

  3. 花括号{ } 里的内容即为函数体的内容, 这里为函数功能实现的过程,这和之前的代码没太大区别, 之前我们把代码写在 main ()函数里,现在只是把这些写到别的函数里.

  4. 返回值

函数的返回值是通过函数中的 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 

void print_test()
{
	printf("this is for test\n");
}

int main()
{
	print_test();	// print_test函数的调用

	return 0:
}
  1. 进入main()函数
  2. 调用print_test()函数:
    1. 它会在main()函数的前寻找有没有一个名字叫“print_test”的函数定义;
    2. 如果找到, 接着检查函数的参数, 这里调用函数时没有传参, 函数定义也没有形参, 参数类型匹配;
    3. 开始执行print_test()函数,这时候,main()函数里面的执行会阻塞( 停 )在print_test()这一行代码,等待print_test()函数的执行。
  3. 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 有参函数调用

  1. 如果是参列表包含多个实参, 则各参数间用逗号隔开.

    // 函数的定义
    void test(int a, int b)
    {
    }
    
    int main()
    {
    	int p = 10, q = 20;
    	test(p, q);	// 函数的调用
    
    	return 0;
    }
    
  2. 实参与形参的个数应相等, 类型应匹配. 实参与形参按顺序对应, 一对一地传递数据.

  3. 实参可以是常量,变量,或表达式, **无论是餐是何种类型的量, 在进行函数调用时, 他们都必须具有确定的值, 以便把这些值传送给形参. **所以, 这里的变脸是在圆括号 ( ) 外面定义好, 赋好值的变量.

    // 函数的定义
    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 函数返回值

  1. 如果函数定义没有返回值, 函数调用时不能写 void 关键字, 调用函数时也不能接收函数的返回值.

    // 函数的定义
    void test()
    {
    }
    
    int main()
    {
    	// 函数的调用
    	test(); // right
    	void test(); // error, void关键字只能出现在定义,不可能出现在调用的地方
    	int a = test();	// error, 函数定义根本就没有返回值
    
    	return 0;
    }
    
  2. 如果函数定义有返回值,这个返回值我们根据用户需要可用可不用, 假如需要使用函数返回值, 我们需要定义一个匹配类型的变量来接受

    // 函数的定义, 返回值为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 

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;
}
函数定义和声明的区别:
  1. 定义是指对函数功能的确定, 包括指定函数名, 函数类型, 形参及其类型, 函数体等, 他是一个完整的, 独立的函数单位.
  2. 生命的作用则是把韩叔的名字, 函数类型以及形参的个数,类型和顺序通知编译系统, 以便在对包含函数调用的语句进行编译时,据此对其进行对照检查 (例如函数名是否正确, 实参与形参的类型和个数是否一致).

6.5 main 函数与 exit 函数

在 main 函数中调用 exit 和 return 结果是一样的, 但在职函数中调用 return 只是代表子函数终止了, 在子函数中调用 exit ,那么程序终止.

#include 
#include 

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 
#include 
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 

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 

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 指针加减运算

  1. 加法运算
    • 指针计算不是简单的整数相加
    • 如果是一个 int * , +1 的结果是增加一个 int 的大小
    • 如果是一个 char * , +1 的结果是增加一个 char 的大小
#include 

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 

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. 减法运算

    示例1:

    #include 
    
    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 
    
    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 

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 

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 

//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 

int a = 10;

int *getA()
{
	return &a;
}


int main()
{
	*( getA() ) = 111;
	printf("a = %d\n", a);

	return 0;
}

7.6 指针和字符串

7.6.1 字符指针

#include 

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 

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 
#include 
#include 

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 

//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的指针的指针,二级指针

你可能感兴趣的:(自学总结)