章节目录
1.函数杂谈
2.左右值引用与局部变量
3.函数形参
4.函数返回值
前言
今天在看数据结构与算法分析c++描述的函数部分,讲到了为了进一步的空间优化,C11以后的引用提供了引用参数
因此整理一下主要形参和返回值的问题
1.函数杂谈
形参与返回值可以直接翻到后面
函数主要有四要素 函数名 形参表 算法 返回值
这四点如果有不明白的可以出门左拐报个基础课或者必应-find-search
那么接下来我们就要联想到数学意义上的函数了
f(x) = kx +b
那我们可以惊奇的发现数学的函数也有四要素 函数名 表达式(参数方程更合适) 定义域 值域
那我们接下来可以对比一下两者:
数学函数的意义为何?
定义域和值域间满足双射关系的映射
就像普林斯顿微积分说的意义函数其实就是i-o(input-output)关系
对于任何的输入我们都能得到一个输出
当然对于没有输入得到输出,或者反之都是映射关系的违法
那么程序语言的函数呢?
这里我们需要回想一下面向对象的接口,
我们先举个例子
_int sum(int a,int b);//get the sum of the a,b;__
开发者向使用者提供了一个加法函数,说明只要给我两个加数我就可以给你两个数的和
那我们来整理一下sum(a,b) = a+b;
眼熟不眼熟
通过整理我们发现这个跟数学的函数是几乎一致的
只有一个输入时为一元函数
当参数表长度变长时,就可以扩充为复杂的多元函数了
而定义域则是函数的输入比如指定输入数据的类型
输出则是值域
而不同点其实是在于计算机与数学的不同性,
通过数学算法我们可以将原本仅限于数字的维度升维到整个世界,
又或者将整个世界降维到数学世界又或者一台小小的计算机中
大白话说就是由于类的封装特性,
通过不断的封装可以表示一切(尽量能被数据表示)的事物
因此通过这无穷的基向量,我们可以用计算机张成整个世界
因此定义域与值域就扩充到了一种无穷的维度
回到正题这样我们大概就能完成一个数学函数与编程函数的映射了
那么接下来多插一句嘴的就是等号右边的表达式了
在编程语言中我们管右边为右值,右值是一个很重要的概念,下面会考。
右值为表达式,诸如 四则远算,复合运算,多项式运算等(变量本身也算表达式)
那我们会发现表达式数值皆为已知,所以其本身其实就是常量
PS:如果存在未知量就变成方程或者不等式了
那我们回想一下算法的定义
有穷次运算,结果确定,可以被实现
那么我们发现————算法可以被封装为表达式,或者就是表达式,或者复杂的矩阵方程组更合理
那么我们对程序设计的函数大概清晰明了
Type result = name(typea a,typeb b,······)
函数具有 定义域 值域 函数名 算法 四部分
2左右值引用与局部变量
本小结主要整理的左右值引用和局部变量的一些浅显的机制与原理
2-1生存周期
生存周期是指 变量创建->申请与分配内存->变量使用->回收内存
而一般讨论局部变量我们都会讨论局部变量的生命周期
因为局部变量的生存周期存在于当前代码块中,当前代码块执行结束
该部分内存就会被回收
2-2局部变量
而最为特殊的局部变量,也可以说最短命的局部变量,
就是我们常说的临时变量,也就是右值,也是常量具有固定的值不会发生变化
临时被生成或者返回,然后被使用或者赋值,用完就丢没有人知道他们到底在哪里
当然具体的奥秘可能需要学习汇编和编译
2-3左右值引用
引用是指针的语法糖,语法糖或者指针不懂的出门左拐
因此引用的原理其实就是指向目标地址
但不同也存在不同的地方
- 引用由于是语法糖所以有更好的书写性,用法相当于变量的别名,
直接如同正常的变量进行使用即可,无需像原有的指针一样处理复杂的内存,
以及地址与变量的混乱 - 引用只能初始化一次,无法进行赋值,相当于常量指针,所以不像原有指针那样赋值方便,
有很好的地址操作性,新的指针特性应当是迭代器更安全
CODE-1:
int &num = n;//初始化一次
cin>>num;
cout<<num<<endl<<n<<endl;
CODE-2:
int #
OUTPUT:
[Error] 'num' declared as reference but not initialized
code-1执行过后我们会发现如之前所说引用用法与指向的变量一致,如同变量的外号一样
而code-2则证明了引用如果常量指针一样需要初始化且不能重复赋值(其实由于引用为变量的别名所以本身就不能再次赋值了)
PS:变量名的封装awesome,值得一提的是typedef也是另一种封装是针对于类型的封装
而宏定义的封装范围更广,数据 类型,变量名皆可,但由于前者的属于语言所以更安全后者由于预处理
所以不是很安全对于命名和作用域要求高(保证作用域与其他代码块互斥)
最后我们要说明的就是对于常量也就是上文我们提到的右值的引用,
而这里右值其实也是之前在局部变量部分提到的临时变量,
在之前我们说过临时变量生存周期是最短的,如同昙花一般,
创建(绽放),赋值(观赏),撤销(凋零),虽然生命短暂,
But because of short,Each of ms is efficient.
回到正题,那我们在这个部分其实关心的是————临时变量的这个地址是怎样的,会与正常的变量有什么区别
那首先要说明的是 右值引用的写法是比原本多了一个引用符号 int &&num = 1;
2-4地址的分配
2.4.1class定义
Code-3:
class test{
private:
int num;
public:
test(int num=0){//A
this->num = num;
cout<<this<<" was created"<<endl;
}
test(const test& a){//B
cout<<"clone "<<this<<" was created"<<endl;
}
int getnum(){
return this->num;
}
~test(){
cout<<this<<" is delete"<<endl;
}
};
Code-3这里我发现还是不得不说明一下类的构造,深拷贝,和析构
- A段为类的构造函数
类在创建时,都会调用构造函数进行创建,并对相应的成员数据进行初始化,
构造函数分为有参和无参,声明一个变量时调用的就是无参构造 - B段为类的深拷贝
深拷贝即为当新生命的对象进行对象的复制时(重复赋值是=运算符),由于某些成员数据的特殊性如引用只能初始化一次且必须
默认的构造不能很好的进行复制,因此需要进行深拷贝函数的重写 - C段为类的析构函数
析构函数同深拷贝一样,是在回收之前自动进行调用,由于像指针这样的特殊成员数据需要安全的释放,
所以重写析构函数在回收前进行相应操作
2.4.2地址初探
本节开始使用之前讲过的右值引用和普通变量的声明来探索临时变量与普通变量在地址分配的异同点,如 code-4所示
(1)离散型数据地址
Code-4:
#include <bits/stdc++.h>
using namespace std;
class test{
private:
int num;//内部成员
public:
test(int num=0){//可选参数同时重写有无参构造函数
this->num = num;
}
int getnum(){
return this->num;
}
void setnum(int num){
this->num = num;
}
};
template <class T>
void testing(T e,string s){
cout<<"Type was "<<s<<endl<<"sizeof s was "<<sizeof(T)<<endl<<endl;
T &&a = T();//右值引用
T &&b = T();
T c = T(3);
T d = T(4);
cout<<"left value:"<<endl;
cout<<"address of a:"<<&a<<endl;
cout<<"address of b:"<<&b<<endl<<endl;
cout<<"right value:"<<endl;
cout<<"address of c:"<<&c<<endl;
cout<<"address of d:"<<&d<<endl;
cout<<"address of a plus the size of a:"<<&a+1<<endl;
}
int main(){
//A
test t;
//B
long long a = 1;
int b = 3;
long double c = 1;
testing(t,"test") ;
testing(b,"int");
testing(a,"long long");
testing(c,"long double");
return 0;
}
OUTPUT
Type was test
sizeof s was 4
left value:
address of a:0x70fd20
address of b:0x70fd30
address of a plus the size of a:0x70fd24
Type was int
sizeof s was 4
left value:
address of a:0x70fd38
address of b:0x70fd3c
address of a plus the size of a:0x70fd3c
Type was long long
sizeof s was 8
left value:
address of a:0x70fd30
address of b:0x70fd38
address of a plus the size of a:0x70fd38
Type was long double
sizeof s was 16
left value:
address of a:0x70fd20
address of b:0x70fd30
address of a plus the size of a:0x70fd30
A部分我们测试的是自定义类,B部分测试的是基本类型
通过总结我们发现:
- 右值或者说临时变量的内存分配是往后的,而左值变量分配是往前的
- 回想一下程序设计中数组的定义一个所有元素地址连续的集合,所以相邻元素地址+“1”
(注意这里是“1”实际是加1字长,组成原理部分PC值自增知识点)
因此如输出所示基本类型的右值引用加一恰好是下一个分配的地址,也恰好等于地址加字长 - 而与2相反的是“集合”类型的数据地址分配却并不是+1那么接下来我们会对这个问题进行实验。
(2)集合型地址
sizeof(class) = Sum of the size of all members who is data;
类的字长等于所有数据成员的和
因此本节针对于集合类数据,通过增加类的字长来研究集合类的分配机制
3.形参
形参部分的讲述我们还是要回到我们熟悉的数学函数方面
我们之前说对于具有多个形参的函数其实与数学意义上的多元参数方程是一致的
f(x,a,b,c) = ax^2+bx+c;
本小节我们主要的关注点在于实参的传入问题
我们来考虑一个问题,数学意义层面上我们的参数传入,参数本身的值会变化吗
我们之前说过,函数的参数是已知的当存在未知量时其实就变成方程了
因此实际我们使用函数时用的都是常量值,比如,sin(60 degree),ln5.
而根据定义显然我们的常量是不可修改的,
不可能我们计算ln5之后5会变成其他值,要是成了数学世界就大乱了。
那么回到面向对象的接口上,我们再举个例子,
food cook(chef chef1,ingred ingred1,sting foodname);
这里我们定义了一个做饭的接口,这个接口只需要chef1厨师和ingred1食材表以及菜名
就可以得到一道菜了
那我们想象一下我们让甲厨师拿好菜谱和原料去做菜,食材固然会消失
但显然厨师和菜谱并不是一次性物品,并不会消失或者变成其他的物品
因此在编程设计时我们要解决实参与形参之间的这种问题。
这种解决方法我习惯称为形参实例化(总有人怼我说我没学过面向对象只有对象才能叫实例化)
其原理其实可以从局部变量处理解,既然我不能改变实参的值,那我就进行一个克隆来进行实参的保护。
- 调用函数时先会声明“形参”然后将实参克隆过来(绽放)
- 使用“形参”的实例化对象进行对应操作(观赏)
- 使用结束回收实例化对象(凋零)
#include <bits/stdc++.h>
using namespace std;
void out(){
cout<<"out"<<endl;
}
void sum(test a){
out();
}
using namespace std;
int main(){
test a;
sum(a);
return 0;
}
OUTPUT:
0x6ffdf0 was created
clone 0x6ffe00 was created
out
0x6ffe00 is delete
0x6ffdf0 is delete
通过之前的讲解以及输出我们可以很清楚的发现,
形参果然要实例化,并且调用的是深拷贝,最后运行结束所有语句后回收内存
现在明晰了形参实例化的原理,
那我们又发现了一个问题,
每次调用都要拿相同的空间生成这样的数据甚至于int data[n][n]
这样的集合类数据,我们要用O(sizeof(data))的空间与时间,时空损耗都很高
因此我们发现我们为什么不直接把变量拿进来呢,而不是像原本数学函数那样定义参数都是右值
因此在c语言时经典的swap问题我们采用了指针的形势
#include <iostream>
using namespace std;
int &sum(int a,int b){
int num = a+b;
int &num1 = num;
return num1;
}
int main()
{
int &n = sum(3,4);
cout<<n<<endl;
cout<<&n<<endl;
sum(1,2);
cout<<n<<endl;
return 0;
}
因此编译器在发现该内存使用未结束后并没有回收局部变量而是等待最后内存完全使用结束才回收
安哥好棒,讲解得很详细!
淦其实我更喜欢建议