当前位置: 首页 > news >正文

c++基础(三)

12.函数

1.1 函数的定义与使用
1.2 函数的参数传递
1.3 变量的生命周期
1.4 函数的其他特性
1.5 函数的嵌套与递归调用

12.1 函数的定义与使用

函数:在程序设计时,如果一段代码重复进行某个操作或者完成一个特定的功能,则将这样的代码组织成为函数,方便代码的复用。定义函数:函数类型 函数名(形式参数列表){语句组;}函数类型、函数名和函数参数列表称为函数头,函数名遵守标识符的命名规则。函数体由两个大括号括起来,并且结尾不能加分号。

函数类型与返回值:

函数可以有返回值,也可以没有函数类型就是返回值的类型,可以是C++支持的任何类型函数返回值由函数体中的return语句给出函数不需要返回值时,函数类型要用void类型指定

函数参数:

形参(形式参数):定义函数时括号中的参数实参(实际参数):调用函数时括号中的参数

函数调用:

函数名(实际参数列表);

函数声明:

用于声明一个函数,但并未实现这个函数,并未定义。主要用于告诉编译器这是个函数。因为有时候我们在调用函数的时候,函数的实现实在后面定义的。语法:函数类型 函数名(形式参数列表);//省略了函数体的部分eg://函数声明int myMax(int x, int y);	

12.2 函数的参数传递

参数传递就是在函数调用的时候,将实参传递给形参的过程。函数实参可以是变量、常量、表达式。在函数调用时,系统为形参分配内存单元,并将实参的值传到形参的内存单元中。根据需要,函数参数可以选择数值、指针、引用三种形式传参:1)数值作为函数的参数:实参传值给形参,通过代码验证,数值作为函数参数,不能实现交换主函数中两个变量的值。因为实参和形参是不同的地址。2)指针作为函数的参数:函数参数不仅可以是整型、浮点型等变量,也可以是指针类型,指针作为函数参数,实际上传递的是变量的地址。3)引用作为函数的参数:在函数中想改变主函数中变量的值,除了可以使用指针传参,还可以使用引用传参来实现。因为引用本质上也是拿到了地址,是通过指针常量实现的,所以跟指针的操作是一样的。引用传参和值传参的区别:引用作为函数参数,调用时的实参只能是变量,不能是常量或者表达式。原因是:引用在创建时就必须初始化,它代表的就是某个内存地址。而常量或者表达式本身并不具备特定的内存地址,所以只能让引用绑定到变量的地址。数组作为函数参数,调用时实参既可以是变量,也可以是常量或表达式。引用作为函数参数,在函数中可以改变主调函数传过来的实参值。另外,当参数是复杂类型(类、结构体等自定义类型)时,使用引用传参会节省时间和空间。

数组作为函数的参数

数组名作为参数实际上传递的时数组的首个元素地址。关于数组的返回值:C++中函数时不能直接返回一个数组的,但是数组其实就是指针,所以可以让函数返回指针来实现返回一个数组。

12.3 变量的生命周期

每个标识符(变量、常量等)都用确定性的作用域位置,决定他们在什么范围内有效(能被访问)。变量的作用域,即变量的有效范围。变量作用域决定了变量的可见性(空间概念)和生命周期(时间概念)。栈内存的局部变量离开它的作用域后,会被编译器回收释放。而堆内存的变量需要程序员自己管理,自己负责回收。内部变量和外部变量在一个函数内部定义的变量是内部变量(局部变量),它只在该函数范围内有效(函数的大括号内)。在函数外定义的变量称为外部变量(全局变量),它在整个文件内有效。全局变量在任何函数中都可以访问,函数中的局部变量只在该函数中可以访问,复合语句(如循环体)中的局部变量只在复合语句中可以访问。所以全局变量作用域最大,复合语句中的局部变量作用域最小。如果在一段程序中,我们在不同作用域定义了多个同名变量,如果都能访问,则优先访问作用域最小的变量。如果要指定访问跟局部变量同名的全局变量,可以在变量名前加作用域符号::全局变量,在整个文件内有效;
int i = 1;
void fun()
{int i = 10;//i是fun函数的局部变量,只在fun的打括号内有效,跟全局变量同名cout << "fun函数的局部变量i=" << i << endl;//10cout << "全局变量i=" << ::i << endl;//1
}int i = 1;
void test07()
{cout << "全局变量i=" << i << endl;//全局变量1int i = 5;//test07的局部变量//复合语句,一个对大括号,块作用域{int i = 7;//属于大括号这一块的作用域cout << "大括号块作用域变量i=" << i << endl;//7cout << "全局变量i=" << ::i << endl;//1}cout << "test07函数的局部变量i=" << i << endl;//5cout << "全局变量i=" << ::i << endl;//1fun();//调用fun函数,它也有i变量
}

变量的存储类别:

静态存储:

静态存储变量是指在程序运行期间,分配固定的存储空间并且让它一直有效(在程序运行期间,变量始终存在)静态变量的定义:static 数据类型 变量名;

动态存储:

动态存储变量是指在程序指向过程中,根据需要动态分配存储空间。例如函数中的局部变量,当程序执行到该函数时,就为局部变量分配空间,当程序离开函数时,函数中的局部变量就被释放,下一次执行到该函数时,再重新为局部变量分配存储空间。(执行到变量所在模块,变量存在),这是通过编译器自动完成。int fact(int n)
{//定义静态变量,它只定义一次,不会被回收,一直有效,当函数再次被调用的时候,它仍然存在,而且保留了历史值static int f = 1;//初始化为1,由于是静态变量,f只会被初始化一次,保存某个数的阶乘,f保存的值是一直存在,当我们计算n的阶乘的时候,如果知道了n-1的阶乘,只需要让n-1的阶乘乘以n,计算一次就够。反之,如果使用普通局部变量,由于函数每次调用完之后会被回收,再次调用函数会重新分配变量的值,重新初始化为1,所以要不停重复计算。f *= n;//f保留了n-1的阶乘结果,只需要再乘以n即可return f;
}
void test08()
{for (int i = 1; i <= 10; i++){cout << i << "的阶乘=" << fact(i) << endl;}
}

12.4 函数的其他特性

内联函数:

使用函数有利于代码的重用,可以提高程序的可读性,增强程序的可靠性。但是使用函数也会降低程序执行的效率,因为调用函数是有开销的,要进行参数传递、控制转移。对于一些规模较小而且需要频繁调用的函数,为了避免这种开销,提高执行效率,诞生了内联函数。内联函数不是在调用时发生控制转移,而是在程序编译时被调用的内联函数就嵌入到每个函数的调用处。节省了函数调用的开销。说明:不是所有声明为内联函数后就可以成功编译为内联函数,因为内联函数如果过大,它会造成编译后的可执行文件过大。所以只有简单的函数才可以被编译为内联函数,内联函数不能含有循环、分支结构,否则编译器也会把它当普通函数处理。定义语法:inline 函数类型 函数名(参数表){函数体代码}

函数重载:

可以定义多个同名函数,只要它们的形参个数或者类型不同就可以同时使用,编译器会根据实参与形参的类型以及个数,自动确定调用哪个函数。这就是函数重载,这些同名函数称为重载函数。函数的返回值类型不能作为重载的依据的。

带默认参数值的函数:

在函数调用时,必须为函数提供与形参个数和类型一致的实参,否则编译器会报错。但是在定义函数时,可以预先为函数参数指定默认值。在调用函数时,如果给出了实参,则会采用实参值,如果没给出实参,则会采用默认值。重点:参数的默认值必须由右向左的顺序来给出。如果某个参数有默认,则它右侧的参数必须也有默认值。只有这样才可以做到传参的时候,省略参数,使用默认值。函数可以声明后,再定义,此时参数默认值只能在声明或者定义的一个位置给出。建议在声明的位置给出。

函数指针:

程序中定义的变量会占用内存空间,同样函数也需要占用内存空间,每个函数所占用的内存起始地址就是函数的地址,同样,函数名就是函数的地址。可以定义用于保存函数地址的指针变量,使用这个指针变量调用函数。简称函数指针。语法:函数类型 (*函数指针名)(参数表);//需要注意,函数指针中的返回类型和参数表要和指针指向的函数保持一致。//定义内联函数,很简单的加法运算,计算两个数的和
inline int add(int a, int b)
{return a + b;
}
void test09()
{int a=10, b=20, c;c = add(a, b);cout << "add的返回值:" << c << endl;c = add(a, 50);cout << "add的返回值:" << c << endl;c = add(50, 50);cout << "add的返回值:" << c << endl;
}//函数重载
//三个同名函数,用于比较大小,它们的参数不同
int max(int x, int y)
{cout << "调用int参数的max函数" << endl;return x > y ? x:y;//条件运算符实现
}
double max(double x, double y)
{cout << "调用double参数的max函数" << endl;return x > y ? x : y;//条件运算符实现
}
char max(char x, char y)
{cout << "调用char参数的max函数" << endl;return x > y ? x : y;//条件运算符实现
}
void test10()
{int a = 10, b = 20, c;double x = 24.62, y = 89.3, z;char ch1 = 'A', ch2 = 'B', ch;c = max(a, b);z = max(x, y);ch = max(ch1, ch2);cout << c << "," << z << "," << ch << endl;
}//带默认参数值的函数:
/*
1、编写一个求x的n次方的函数(n是正整数),带有默认的参数值。
*/
long defaultpower(int x=10, int n=2)
{long res = 1;for (int i = 1; i <= n; i++){res *= x;}return res;
}
void test11()
{cout << defaultpower(2, 3) << endl;//传入了实参cout << defaultpower(2) << endl;//传入了实参,但只传了一个,后面那个取默认值cout << defaultpower() << endl;//一个都不传,全部取默认值
}//函数指针://定义函数指针fp,指向某个具有两个int参数并且返回int值的函数void test12()
{
long (*fp)(int, int);
fp = defaultpower;
int a = 2, b = 3;
cout << "较大值:" << fp(10, 2) << "," << fp(a, b) << endl;
cout << "defaultpower函数的地址" << defaultpower << endl;
}

12.5 函数的嵌套与递归调用

函数嵌套调用:

C++中函数的定义都是相互独立的,不允许在函数中定义另一个函数,不允许嵌套定义。但允许在一个函数中对另一个函数进行调用,这就是嵌套调用。

函数递归调用:

一个函数不仅可以调用其他函数,还可以调用它本身。一个函数在它的函数体内,直接或间接地调用它本身,称为递归调用,这种函数称为递归函数。递归求阶乘实现思路:假设我们写了一个函数可以实现某个数字n的阶乘,long power(n),那么我们就可以推导出来,n的阶乘等于n乘以n-1的阶乘;n-1的阶乘等于n-1乘以n-2的阶乘,如果想得到n的阶乘,就要一直往前推导,这个过程叫做回推。至于回推到什么时候才能结束呢?答案是回推到某个数的阶乘不需要计算,不需要调用power函数,有一个确定结果。数学中规定了:1的阶乘就是1,0的阶乘也是1,负数是没有阶乘的。所以只要推到1或者0就可以确定结果了。然后再反着推n的结果,这个过程叫做递推。我们根据0和1的阶乘能得到2的阶乘,再得到3的阶乘,直到得到n的阶乘。递归函数会经历过很多次函数调用,而函数调用是在栈空间完成的,所以递归会占用较多的栈空间。写递归函数的关键点:1.明确基本函数的功能在编写递归函数之前,要明确函数基本功能,包括参数,返回值,函数逻辑。2.确定递归的终止条件递归必须有终止条件,否则会陷入无限递归,最终导致栈空间溢出错误。终止条件就是递归函数停止调用自身的条件。通过是处理最简单的情况。3.找出递归关系递归关系指的是如何将一个大问题分解为一个或多个小问题去解决。4.确保递归调用的参数能逐渐逼近终止条件每次递归调用时,传给递归函数的参数要逐渐向终止条件靠近。计算阶乘的例子中,n*power(n-1),这个n-1参数的设置尤为关键5.注意递归深度递归深度指的是递归函数调用自身的次数,如果深度过大,可能会导致栈溢出。6.处理递归返回值在递归函数中,要正确处理每次递归调用的返回值,并将它用于当前问题的求解。

13.自定义类型与命名空间

9.1 结构类型
9.2 联合类型
9.3 枚举类型
9.4 命名空间
9.5 多文件组织

13.1 结构类型

结构类型属于自定义类型的一种。可以定义一些复合的数据结构。适用于存储一些复杂的数据。比如说,存储一个学生的基本情况,包含很多信息:学号、性别、年龄、成绩等等。这些数据它们的类型不同。所以需要使用结构来保存这种复杂的数据。

定义语法:

struct 结构名{数据类型1 成员变量名1;数据类型2 成员变量名2;...数据类型n 成员变量名n;};使用结构体类型定义结构变量:结构名 结构变量名;访问结构体变量中的成员变量:结构体变量名.成员变量名;

字节对齐:

由于我们采用了复合数据结构,其中包含很多不同类型的数据,那么这个复合数据结构的大小怎么计算呢?并不是简单的加法。而是遵循一定的规则对各类数据进行对齐,对齐即按照内存地址有选择性地存储。主要是为了提高数据存储和访问的效率。这个过程是编译器完成的。基本规则:结构体的成员变量按照它们在结构体中声明的顺序一次存储。每个成员变量都有自己的对齐要求,即内存起始地址是其数据类型大小的整数倍。具体对其过程:结构体中的第一个成员变量,它的偏移量为0,从结构体的起始地址开始存储。后续成员变量的存储位置,要满足自身的对其要求,如果前面的成员变量存储完后,当前位置不满足它的对齐要求,那么就会在它和前面的成员变量之间填充一些字节,以满足对其要求。结构体的总大小,必须是结构体中最大成员变量的整数倍。

结构类型的嵌套与初始化

嵌套即结构体中还有结构体,初始化可以使用大括号{}来初始化结构体。类型别名:就是给数据类型起别名。别名和原名等价,可以使用别名代替原名。主要用于给自定义类型定义更简单的别名。
语法:typedef 类型原名 类型别名;如:typedef struct student S;//使用s代替student

结构数组与结构指针

结构数组:使用结构体变量组成的数组。语法:结构体类型 数组名[常量表达式];结构指针:指向结构体的指针。语法:结构体类型名 *结构指针变量名;//使用结构体数组保存多个学生信息
student s_arr[3] = { {2025110,'f',21,92},{2025111,'m',22,85},{2025112,'f',20,90} };

13.2 联合类型

联合类型也属于一种自定义类型,它的特点是成员变量共用一块内存,所以联合也叫做共用体。
联合在任意时刻,只能有一个数据成员有值。当某个成员赋值后,其他成员就会变成未定义的状态。
当联合中的数据成员只需要多选一去存储,不需要同时存储,就可以采用联合类型。
联合类型的大小等于其中最大的类型大小。
格式:union 联合名{数据类型1 成员变量名1;数据类型2 成员变量名2;...数据类型n 成员变量名n;};

13.3 枚举类型

在控制结构的switch分支结构中已经讲解。

13.4 命名空间

程序大到一定程度之后,通过会分成很多个项目,每个项目可能是由不同的团队来开发维护的。因此很容易产生命名冲突的问题,程序中的标识符容易同名。为了避免命名冲突,C++引入了命名空间(也叫名称空间)机制。它限定了你的标识符在哪个命名空间。

定义语法:

namespace 命名空间名{命名空间中的代码;}命名空间使用:使用域运算符::命名空间名::标识符名

注意事项:

1)命名空间只能在全局范围内去定义,而且命名空间也是可以嵌套的。2)命名空间也可以有别名,使用如下语法:namespace 别名=命名空间原名;命名空间的使用:
常用的做法是,在头部声明要使用的命名空间,具体有两种做法:1)使用using声明要使用哪个命名空间的哪个标识符,标识符一般是个函数。这样就可以直接在当前文件中使用这个标识符,
而没有声明的标识符,就需要在标识符前面显式地声明命名空间的名称。注意:声明之前需要先定义好命名空间。2)使用using编译指令using namespace 命名空间名;表示要使用整个命名空间中所有的标识符,这样所有这个命名空间中的标识符都可以直接使用了。
C++标准库:
C++标准库被所有的C++编译器支持,都包含在一个单一的命名空间中:std
所以我们需要使用using namespace std;才可以使用标准库中的函数如:cout、cin、endl

13.5 多文件组织

前面学习的程序都是在一个文件中完成的,但实际上项目中通常包括大量的程序文件和各种资源文件,这些文件分别有各自的用处,我们要将它们组织到一起称为整个项目。

这里只讨论最相关的两种文件:头文件(.h结尾的)还有源文件(.cpp结尾的)

头文件:用于存放结构的定义、函数的原型声明、全局变量、静态变量、常量等,还有即将学习的类的声明。主要用于存放声明类的信息。源文件:主要用于存放实现类的信息,比如对函数的实现,对类中成员函数的实现等等,具体的代码业务逻辑等。练习:编写一个处理平面图形的程序,可以处理各种图形,计算它们的面积周长等,用多文件组织起来。设计一个表示点的结构Point,有两个int成员表示坐标。设计一个表示圆形的结构Circle,包含一个Point成员表示圆心,还有一个double成员表示半径。设计一个表示矩形的结构Rectangle,包含一个Point成员表示矩形的中心,和两个double成员表示长和宽。文件组织:头文件:Point.h:表示Point结构的定义Circle.h:Circle结构的定义,以及跟它相关的函数原型声明Rectangle.h:Rectangle结构的定义,以及跟它相关的函数原型声明源文件:Circle.cpp:与Circle结构有关的函数的定义Rectangle.cpp:与Rectangle结构有关的函数的定义Main.cpp:主函数所在的文件

编译预处理指令(宏)

文件包含:#include <文件名> 或者 #include "文件名"宏定义:#define 标识符 字符串 如:#define PI 3.1415926条件编译:允许根据不同的条件对代码的不同部分进行选择性编译。#ifdef #ifndef #else #elif #endif1)#ifdef,#else,#endif指令:ifdef用于检查某个宏是否已定义,如果已定义,就编译#ifdef和#endif之间的代码,否则就不编译2)ifndine,#endif指令:与#ifdef相反,检查某个宏是否未定义。如果未定义,就编译就编译#ifndef和#endif之间的代码。主要是防止头文件被重复包含。VS创建头文件的时候,帮我们生成了预处理语句#pragma once,它的作用就是防止头文件重复包含的,那为什么还要使用经典的#ifndef方式呢?原因是#pragma once不具有通用性,如何换了编译器或者换了平台比如操作系统,可能就无法使用。而经典的#ifndef方式是通用的。
http://www.lqws.cn/news/103807.html

相关文章:

  • Trae CN IDE自动生成注释功能测试与效率提升全解析
  • Linux: network : switch:hp5500
  • 情趣私域运营:打造高效转化的私域营销体系
  • 【Redis】笔记|第7节|大厂生产级Redis高并发分布式锁实战(二)
  • 第11节 Node.js 模块系统
  • WebRTC中sdp多媒体会话协议报文详细解读
  • 法律大语言模型(Legal LLM)技术架构
  • Selenium 中 JavaScript 点击操作的原理及应用
  • Nginx+Tomcat 负载均衡、动静分离
  • 设计模式-原型模式
  • Java面试八股--08-数据结构和算法篇
  • 0518蚂蚁暑期实习上机考试题1:数组操作
  • go get下载三方库异常
  • STM32入门教程——按键控制LED光敏传感器控制蜂鸣器
  • Leetcode 261. 以图判树
  • ​链表题解——回文链表【LeetCode】
  • Java基础 Day28 完结篇
  • 使用 Golang `testing/quick` 包进行高效随机测试的实战指南
  • Elasticsearch集群最大分片数设置详解:从问题到解决方案
  • Mac电脑_钥匙串操作选项变灰的情况下如何删除?
  • React-native之Flexbox
  • 相机Camera日志分析之二十四:高通相机Camx 基于预览1帧的process_capture_request三级日志分析详解
  • torch.distributed.launch 、 torchrun 和 torch.distributed.run 无法与 nohup 兼容
  • Redis:常用数据结构 单线程模型
  • 【Typst】3.Typst脚本语法
  • 使用Redis作为缓存优化ElasticSearch读写性能
  • AutoGenTestCase - 借助AI大模型生成测试用例
  • 批量大数据并发处理中的内存安全与高效调度设计(以Qt为例)
  • 基于Matlab实现LDA算法
  • MySQL 全量、增量备份与恢复