大学C++编程之函数

前言:
有些话必须提前说。。。
作为一个上了大学后就再也没认真听过程序设计课的竞赛老狗来说
最近,我竟然在课上发现了一些我闻所未闻的崭新姿势(震惊!!!)

这些内容都囊括在了C++大学教程的第六章中,因此以下我做出了一些讲解总结

有一说一:
本次的内容在中学竞赛中基本毫无用处,但是作为一只未来一定会秃头的程序姬,这些又是不必可少的知识技能

整篇blog属于高不成低不就的水准
可能对初学者不是很友好,只能作为有基础的同学的思维导图和拓展材料

废话不多说,冲冲冲~


什么是函数?

宛如zz的问题,竞赛老狗都不屑于回答你~
简单来讲,就是能够实现一些特定功能function
函数可以笼统而随意地分为C++自带函数,以及程序员自定义函数

首先来点干货:C++中部分简单内置函数


函数原型

声明一个正确函数,我们需要告诉编译器一些什么呢?
函数名,返回类型,参数个数,参数类型,参数顺序
因此在程序中,我们声明函数必须使用这种特定的形式:

函数类型 函数名 ( 含类型说明的形式参数表 )

其中,

函数名(含类型说明的形式参数表)

称为函数签名

值得注意的是:

  • 函数签名不包含函数(返回)类型
  • 函数签名在同一作用域内唯一
    函数作用域: 程序中函数允许被访问的范围
在这里我们可以注意到,编译器要求的是“函数签名唯一”,而不是“函数名唯一”

这就意味着,以下的程序是合法的:

#include<iostream>

using namespace std;

int max(int x,int y) {
	return (x>y)? x:y;
}

int max(int x,int y,int z) {
    int t;
	return (t=(x>y)? x:y)>z? t:z; 
}

int main()
{
	cout<<max(1,2)<<endl;
	cout<<max(1,2,3)<<endl;
	system("pause");
	return 0;
}

在这个程序中,编译器会自动根据传入的实参个数锁定正确的函数
由此可以看出,定义函数时真正起决定作用的时函数签名而不是函数名
(不禁感叹C++真的强)


插播一条广告

强制类型转换
长期使用C++的老狗都会明白什么是:

int sum=0;
...
double ans=(sum*1.0)/2.0;

实际上C++中还有一种更正规更稳定的类型转换方式:

int sum=0;
...
double ans=static_cast<double>(sum)/2.0;
你问我为什么要插播这条广告?

举个简单的栗子:
我们在使用C++自带函数 s q r t sqrt 时,无意中会在括号内传入一个整形:

sqrt(4)
//ans=2

看见输出的结果正确,就拍拍屁股走人了

实际上 s q r t sqrt 这个函数的原型是这样的:

double sqrt(double)

好,是不是发现问题了?
是函数原型告诉编译器将整形的4转换为double类型后再进行运算

一般来说,与函数原型中参数类型不完全相符的参数值转换为正确类型之后在进行函数调用

这就引发了另一个真理问题的讨论:
一种类型转换为另一种类型时,怎样才能保证数据不丢失?

转换规则:
混合类型表达式(包括两种或多种数据类型的表达式)中,每个值的参数类型提升为表达式中的最高类型

在这里插入图片描述
也就是说,所占字节越高,数据类型储存的信息量越大
再类型转换时我们只往高不往低,保证数据不会丢失


函数的形参(paramaters)和实参(arguments)

形式参数
函数原型中声明的变量或者是函数定义时声明的变量
实际参数
函数被调用时代替形式参数传入函数的值

当函数被调用时,所有的形参以变量形式被创建,实参的值被赋给变量(形参)

void doit(int x);  //x is a parameter
...
doit(x); //x is an argument

头文件

标准函数库
  • 从C语言中继承下来
  • C格式的输入输出函数,字符与字符串处理函数,数学函数,时间日期函数,动态分配函数以及一些实用函数
标准类库
  • 标准C++的I/O流类,字符串类,数字类,异常处理和杂项类以及STL容器类
C++的头文件来源
  • 标准C语言库函数的头文件,带 . h .h 后缀
    • include < string.h >
  • 标准C++语言类库的头文件,不带 . h .h 后缀
    • include < iostream >
  • 由标准C语言库函数头文件变成 的标准C++头文件,把原有标准C语言库函数头文件去掉 . h .h 后缀,加上 c c 前缀
    • include < cstring >

C++标准库的所有头文件都没有扩展名

头文件分类

语言支持:标准库中与语言支持功能相关的头文件
输入输出:支持流输入输出的头文件
诊断:与诊断功能相关的头文件
一般工具:定义工具函数的头文件
字符串:支持字符串处理的头文件
容器:定义容器类的模板的头文件
迭代器支持:支持迭代器的头文件
算法:有关算法的头文件
数值操作:有关数值操作的头文件
本地化:有关本地化的头文件

常用头文件(原谅我偷个懒)

存储类别&&作用域&&生存时间

介绍这个部分,必须从一些基本概念说起:

标识符
  • 在程序中提供给变量,类型函数与标签的名称
  • 所有标识符必须由一个字母或下划线开头
  • 标识符的其他部分可以由字母,下划线或数字组成
  • 区分大小写
常量
  • 在程序执行过程中不变的数据

  • 常量所需要的内存空间大小由ta的类型决定

  • 分类:

    • 直接常量:23,1,‘A’,‘This is a string’

    • 符号常量:const<类型名><常量名>=<值>
      #define<常量名><值>

变量
  • 在程序运行过程中从程序外部获得或在程序运行过程中通过计算产生的,并且其值会经常发生变化
  • 在内存中占用一个逻辑存储空间
表达式
  • 由操作符,操作数以及圆括号所组成的运算式
  • 单独常量或变量构成了表达式的特例,称为基本表达式
存储类别
  • 标识符的存储类别确定了标识符在内存中存在的时间
    有些标识符的存在时间很短,有些则重复生成和删除,有些存在于整个程序的执行期间
作用域
  • 标识符的作用域是程序中能引用这个标识符的区域
    有些标识符可以在整个程序中引用,有些标识符只能在程序中的有限部分被引用
连接
  • 确定标识符仅在定义ta的源文件中可见,还是在编译,连接的多源文件程序中可见

在这里插入图片描述

在一番枯燥的概念介绍后,我们了解了什么是存储类别
简而言之,不同存储类别的变量在程序中可以存在不同长度的时间
在这里插入图片描述
注:自动变量:局部变量和函数参数

上图展示了四种不同的存储类别,其中比较玄妙的就是静态存储类

静态存储类的声明

程序开始执行时开始存在

S t a t i c ( ) Static(静态):可用于函数中的局部变量

  • 仅在函数体内可见
  • 数字变量默认初始化为0
  • 函数结束时,保留变量的数值
  • 下次调用时,忽视初始化,保留上次函数退出时的值

E x t e r n ( ) Extern(外部):全局变量和函数名

  • 可以在文件中声明或定义的任何函数调用
//test.h
//初次正常定义
int x;
//----------------------------------------------------------------
//在多个文件中使用
//extern告诉编译器变量在某处声明过
//test.h
extern int x=5;
//----------------------------------------------------------------
//main.cpp
#include"test.h"
//包含自定义头文件
int main()
{
    x=7;
    return 0;
}

最后,

注意:
  • 局部变量默认为自动存储类
  • register通常不用声明
  • 一个标识符不能使用多个存储类指示符

讲了这么多云里雾里,实际上这一部分还是关于生存期和作用域的真理问题讨论

下面举一个很值得深入思考的栗子:

#include<iostream>
using namespace std;

void useLocal();
void useStaticLocal();
void useGlobal();

int x=1;

int main()
{
	int x=5;
	cout<<"local x in main's outer scope is "<<x<<endl;
	{  //start new scope
		int x=7;
		cout<<"local x in main's inner scope is "<<x<<endl;
	}
	useLocal();
	useStaticLocal();
	useGlobal();
	useLocal();
	useStaticLocal();
	useGlobal();
	cout<<"\nlocal x in main is "<<x<<endl;
	system("pause");
	return 0;
}

void useLocal() {
	int x=25;
	cout<<"\nlocal x is "<<x<<" on entering useLocal"<<endl;
	x++;
	cout<<"\nlocal x is "<<x<<" on entering useLocal"<<endl;
	return;
}

void useStaticLocal() {    //静态存储类型
	static int x=50;
	cout<<"\nlocal static x is "<<x<<" on entering useLocal"<<endl;
	x++;
	cout<<"\nlocal static x is "<<x<<" on entering useLocal"<<endl;
	return;
}

void useGlobal() {    //静态存储类型
	cout<<"\nglobal x is "<<x<<" on entering useLocal"<<endl;
	x*=10;
	cout<<"\nglobal x is "<<x<<" on entering useLocal"<<endl;
	return;
}

在这里插入图片描述

区别
  • 标识符的作用域是一个静态的概念,由程序的静态文本决定(程序中在哪里定义就在对应区间起作用)
  • 生存期是一个动态的概念,描述程序运行时变量或参数存在(占用内存)的时间段
  • 生存期和标识符作用域有关,但决定因素还是存储类别
    例如:static存储类的局部变量,其作用域为定义它的函数,生存期为整个程序运行时间

基于栈的函数调用和活动记录

这一部分理论性极强,不冗述,放个图表:

函数被调用时 函数被调用后
1. 记录返回地址。 便于CPU知道函数返回后下一条指令的地址 1. 函数返回值被拷贝到临时内存空间
2. 为函数返回值分配临时内存空间 2. 所有局部变量和实参被释放
3. 函数的实参入栈,CPU开始执行函数的内部代码 3. 战中存放的函数返回值被赋给函数调用处,释放相应内存,如果无赋值语句发生,则返回值丢失
4. 函数的局部变量一旦定义,便入栈 4. 返回函数调用点,下一条指令的地址出栈

函数传递默认参数

如果在调用函数时,经常对某个形参使用相同的实参值,那么可以为这个形参设置 D e f a u l t A r g u m e n t Default Argument (默认/缺省实参),即给该形参传递默认参数

缺省值可以是任意表达式,包括常量,全局变量或者函数调用(返回值)等

规则一: 默认实参应在函数名第一次出现时设定,即函数原型(类接口),过着函数定义的函数头部,而且无需也不可重复定义

//Case One:在函数原型中设定默认实参
int Volume(int=1,int=1,int=1);
int main{...}
int Volume(int l,int w,int h) {
    return l*w*h;
}
//Case Two:在函数定义的函数头中设定默认实参
int Volume(int l=1,int w=1,int h=1) {
    return l*w*h;
}
int main{...}
//Error One:
int Volume(int,int,int);
int main{...}
int Volume(int l=1,int w=1,int h=1) {
    return l*w*h;
}
//----------------------------------------------------------------
//Error Two:
int Volume(int l=1,int w=1,int h=1);
int main{...}
int Volume(int l=1,int w=1,int h=1) {
    return l*w*h;
}

规则二: 默认实参必须是函数参数列表中右端(尾部)的参数,即如果某个参数设定了默认参数实参,那么该参数右边的所有参数必须具有默认实参

//OK
int Volume(int l=1,int w=1,int h=1);
int Volume(int l,int w=1,int h=1);
int Volume(int l,int w,int h=1);

//Error
int Volume(int l=1,int w,int h=1);
int Volume(int l,int w=1,int h)l

规则三: 函数调用时,若省略了某个实参,那么编译器会自动给插入默认实参,并且生成新的函数调用。显式传递给函数的实参是从左到右给形参赋值

int Volume(int l=1,int w=1,int h=1);
//1
Volume();         //l=1,w=1,h=1
//2
Volume(10);       //l=10,w=1,h=1
//3
Volume(10,5);     //l=10,w=5,h=1
//4
Volume(10,5,2);   //l=10,w=5,h=2

利用一元作用域解析符访问全局变量

全局变量与局部变量重名,如何访问全局变量?
(我私以为这个问题很蠢,为什么有人想要把全局变量和局部变量起一个名字呢)

Unary Scope Resolution Operator (一元作用域解析符) —— ::
#include<iostream>
using namespace std;

int number=7;

int main()
{
    double number=10.5;
    cout<<"Local double value of number = "<<number
    <<"\nGlobal int value of number = "<<::number<<endl;
    return 0;
}
Local double value of number = 10.5
Global int value of number = 7
即使没有同名变量存在,也建议用::修饰全局变量

函数模板

神仙的讲解
前辈讲的非常清晰系统,我在这里就xue微废话一下(惭愧)

如果多个函数操作的程序逻辑完全相同,仅仅是操作数据类型不同,那么可以使用函数模板

原型:temple关键字 + < template parameter list (模板参数列表)>

模板参数列表:

  • 参数称为formal type parameter (形式类型参数),本质上是占位符,在函数调用时替换为基本数据类型或用户自定义的数据类型
  • 每个参数必须以关键字typename ( 或者class) 起头,参数之间必须以逗号分隔
template <typename T>
T maximum(T a,T b,T c) {
    T ans,t;
    ans=((t=(a>b)?a:b)>c)? t:c;
    return ans;
}

看上去很简单的形式,实际上功能非常强大,支持各种类型的数据:

//1
maximum(4,3,5);
ans=5;
//2
maximum(4.3,2.34,7.14);
ans=7.14;
//3
maximun('a','abb','aba');
ans='abb';

6.13,6.14,6.17(存疑,待续)

发布了941 篇原创文章 · 获赞 192 · 访问量 32万+

猜你喜欢

转载自blog.csdn.net/wu_tongtong/article/details/103392489