第八章 函数探幽

本文深入探讨C++中的函数概念,包括内联函数、引用参数、默认参数、函数重载及模板。详细讲解了内联函数如何提高执行速度,引用参数如何避免数据复制,函数重载和模板如何增强代码灵活性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

程序编译
  • 编译的最终产品是可执行程序——由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机的内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时(有循环或分支语句时),将跳过一些指令,向前或向后跳到指定的地址。常规函数调用也使程序跳到另外一个地址(函数地址),并在函数结束时返回。
  • 执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳回到地址被保存的指令处。来回跳跃并记录位置意味着以前使用函数时,需要一定的开销。
  • C++内联函数提供了另一种选择。
内联函数
  • 内联函数是C++为提高程序运行速度所做的改进。常规函数和内联函数的主要区别在于C++编译器如何将它们组合到程序中。
  • 对于内联函数,编译器将使用相应的函数代码替换函数调用,程序无需跳到另一个位置处执行代码然后再调回来。
  • 因此内联函数的运行速度比常规函数稍快,但代价是需要占用更多的内存。
  • 如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数的10个代码拷贝。
  • 应有选择地使用内联函数。如果执行函数代码的时间比处理函数调用机理的时间长,则所节省的时间将只占很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。
  • 使用方法:在函数声明前加上关键字inline;在函数定义前加上关键字inline。
  • 编译器不一定会满足程序员的内联函数请求。它可能认为该函数过大或注意到函数调用了自己(内联函数不能递归),因此不将其作为内联函数。
  • 内联函数和常规函数一样,也是按值传递参数。如果参数为表达式,则函数将传递表达式的值。
引用变量
  • 引用常量是C++新增的一种复合类型。引用是已定义的变量的别名
  • 引用变量的主要用途是用作函数的形参。通过将引用变量用作参数,函数将使用原始值,而不是其拷贝。
  • 除指针外,引用也为函数处理大型结构提供了一种非常方便的途径。
	int rats;
	cint & rodents = tats; // rodents 是rats的别名
  • 引用必须在声明的时候进行初始化。这也是与指针的差别之一。
  • 引用更接近const指针,必须在创建时初始化,一旦与某个变量关联起来就不会改变。
引用与函数
  • 左值参数是可被引用的数据对象,例如,变量、数组元素、结构成员、引用和被解除引用的指针。
  • 非左值包括字面常量和包含多项的表达式。
  • 如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
  • 将引用参数声明为常量数据的引用的理由:1)使用const可以避免无意中修改数据的编程错误;2)使用const使函数能够处理const和非const实参,否则将只能接受非const数据;3)使用const引用使函数能够正确生成并使用临时变量。
引用与结构
  • 引用的引入主要是为了结构和类,而非基本的内置类型。
  • 使用结构引用只需在声明结构参数时使用引用操作符&即可。通常应该将返回的引用声明为const。
  • 通常,返回机制将返回值复制到临时存储区域中,随后调用程序将访问该区域。然而,返回引用意味着调用程序将直接访问返回值,而不需要拷贝。通常,引用将指向传递给函数的引用,因此调用函数实际上是访问自己的一个变量。
	const sysop & use(sysop & sysopref)
	{
		cout << sysopref.name << " says:\n";
		cout << sysopref.quote << endl;
		sysopref.used++;
		return sysopref;
	}
  • 返回引用的函数实际上是被引用的变量的别名。
  • 返回引用时需要注意,应避免返回当函数终止时不再存在的内存单元引用。同样,也应避免返回指向临时变量的指针。为避免这种问题,一种方法是,返回一个座位参数传递给函数的引用。座位参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。另一种方法是用new来分配新的存储空间。
  • 将const用于引用返回类型,并不意味着指向的结构本身为const,而是意味着不能使用返回的引用来直接修改它指向的结构。
引用与类对象
  • 将类对象传递给函数时,C++通常的做法是使用引用。
	string version1(const string & s1, const string & s2)
	{
		string temp;
		temp = s2 + s1 + s2;
		return temp;
	}
	result = version1(input, "***");
	// temp 是一个新的string对象,只在函数version1()中有效。
	// 因此,将返回指向temp的引用不可行,因此该函数返回类型是string而非引用。
	// 这意味着temp的内容将被复制到一个临时返回存储单元中,然后在main()中,该返回存储单元的内容将被复制到一个名为result的string中。
  • string 类定义了一种 char* 到 string 的转换功能,使得可以使用C-风格字符串来初始化string对象。
  • 如果实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用。
何时使用引用参数
  • 使用引用参数的主要原因有两个:1)程序员能够修改调用函数中的数据对象;2)通过传递引用而不是整个数据对象,可以提高程序的运行速度。
  • 当数据对象较大时(如结构和类对象),第二个原因最重要。
  • 对于使用传递的值而不作修改的函数:
    1. 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
    2. 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
    3. 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。可以节省复制结构所需的时间和空间。
    4. 如果数据对象是类对象,则使用const引用。传递类对象参数的标准方式是按引用传递。
  • 对于修改调用函数中数据的函数:
    1. 如果数据对象是内置数据类型,则使用指针。
    2. 如果数据对象是数组,则只能使用指针。
    3. 如果数据对象是结构,则使用引用或指针。
    4. 如果数据对象是类对象,则使用引用。
默认参数
  • 默认参数是指当函数调用中省略了实参时自动使用的一个值。
  • 必须通过函数原型来设置默认值。
  • 对于带参数列表的函数,必须从右向左添加默认值。
  • 默认参数不是编程方面的重大突破,而只是提供了一种便捷的方式。在设计类时,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。
函数重载
  • 函数多态允许函数可以有多重形式。
  • 函数重载指的是可以有多个同名的函数。
  • 函数重载的关键是函数的参数列表,也称为函数特征标。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则他们的特征标相同,而变量名是无关紧要的。
  • C++允许定义名称相同的函数,条件是他们的特征标不同。
  • 如果参数数目或参数类型不同,则特征标不同。
	void print(const char * str, int width);				// #1
	void print(double d, int width);						// #2
	void print(long l;int width);							// #3
	void print(int i,int width);							// #4
	void print(const char * str );							// #5
	//使用被重载的函数时,需要在函数调用中使用正确的参数类型。
	//例如:
	unsigned int year = 3210;
	print(year, 6);  		//模糊调用
	//此时 print() 不与任何原型匹配,没有匹配的原型并不会自动停止使用其中的某个函数,因为C++将尝试使用标准类型转换强制进行匹配。
	//如果#2原型是唯一的原型,则函数调用print(year, 6)将把year转换为double类型。
	//但上文中有3个将数字作为第一个参数的原型,因此有3种转换year的方式。在这种情况下,C++将拒绝这种函数调用,并将其视为错误。
  • 为了避免混乱,编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标。
  • 特征标,而不是函数类型使得可以对函数进行重载。
	//例如,下面两个声明是互斥的
	long gronk(int n, float m);		//same signatures
	double gronk(int n, float m);	//hence not allowed
	//返回类型可以不同,但特征标也必须不同。
何时使用函数重载
  • 仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。
什么是名称修饰
  • 使用C++开发工具中的编辑器编写和编译程序时,C++编译器将执行一些操作——名称修饰(name decoration)或名称矫正(name mangling),它根据函数原型中指定的形参类型对每个函数名进行加密。
  • C++可以以此用来跟踪每一个重载函数。
函数模板
  • 函数模板是通用的函数描述,也就是说,它们使用通用类型来定义函数,其中的通用类型可以用具体的类型(如int或double)替换。
  • 通过将类型作为参数传递给模板,可使编译器生成该类型的函数。
  • 由于模板允许以通用类型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。
  • 由于类型是用参数表示的,因此模板特效有时也被称为参数化类型(parameterized types)。
  • 函数模板允许以任务类型的方式来定义函数。
	//定义一个交换两个值的函数模板
	template <class Any>  //或者  template <typename Any>
	void Swap(Any &a, Any &b)
	{
		Any temp;
		temp = a;
		a = b;
		b = temp;
	}
	//第一行指出,要建立一个模板,并将类型命名为Any。
	//关键字template和class是必需的,除非可以使用关键字typename代替class。
	//必须使用尖括号。类型名可以任意选择(这里为Any)。
  • 模板并不创建任何函数,而只是告诉编译器如何定义函数。
  • 如果需要多个将同一种算法用于不同类型的函数,请使用模板。
  • 如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字 typename 而不是class。
  • 注意,函数模板不能缩短可执行程序。最终的代码不包含任何模板,而只包含了为程序生成的实际函数。
  • 使用模板的好处是,它使生成多个函数定义更简单、更可靠。
重载的模板
  • 需要多个对不同类型使用同一种算法的函数时,可使用模板。不过,并非所有的类型都使用相同的算法。为满足这种需求,可以重载模板定义。
  • 和常规重载一样,被重载的模板的函数特征标必须不同。
  • 并非所有的模板参数都必须是模板参数类型。
显式具体化
  • 显示具体化:提供一个具体化函数定义,其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。
  • C++标准定义形式:
    1. 对于给定的函数名,可以有非模板函数、模板函数和显示具体化模板函数以及他们的重载版本。
    2. 显示具体化的原型和定义应以 template<> 开头,并通过名称来指出类型。
    3. 具体化将覆盖常规模板,而非模板函数将覆盖具体化和常规模板、
    	struct job
    	{
    		 char name[40];
    		 double salary;
    		 int floor;
    	};
    	//下面是用于交换job结构的非模板函数、模板函数和具体化的原型:
    	//non-template function prototype
    	void Swap(job &, job &);
    	
    	//template prototype
    	template<class Any>
    	void Swap<Any &, Any &>;
    	
    	//explicit specialization for the job type
    	template <> void Swap<job> (job &, job &);
    
实例化和具体化
  • 隐式实例化(Implicit instantiation):在使用模板前之前,编译器不生成模板的声明和定义实例。只有当使用模板时,编译器才根据模板定义生成相应类型的实例。
	//隐式实例化
	int i = 0,j = 1;
	swap(i,j);				//编译器根据参数i,j的类型隐式生成swap<int>(int &a,int &b)的函数定义
	Array<int>arVal;	//编译器更具类型参数隐式生成Array<int>类声明和类函数定义。
  • 显式实例化(explicit instantiation):在使用模板前,编译器更具显式实例化指定的类型生成模板实例。
	//格式
	template typename function<typename>(argulist);
	template class classname<typename>;
	//eg
	template void swap<job>(job &a, job &b);	//编译器会根据原模板的定义和该声明直接生成一个实例函数,该函数仅接受job型。否则编译器遇到模板的使用时才会隐式的生成相应的实例函数。
  • 显式具体化(explicite specialization):对于某些特殊类型,可能不适合模板实现,需要重新定义实现,此时可以使用显式具体化。
	//	格式
	template<>typename function<typename>(argu_list){…};
	template<>class classname<typename>{…};
	//eg
	template<>void swap<job>(job &a,job &b){…};	//意思是job类型不适合用于函数模板swap的定义,因此通过显式具体化重新定义;
	//也可简写为 
	template<> void swap(job &a, job &b);
  • 试图在同一个编程单元中使用同一种类型的显式实例和显式具体化将出错
  • 隐式实例化、显式实例化和显式具体化统称为具体化(specialization)。它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。
编译器选择函数版本
  • 对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定位函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)。
  • 重载解析过程:
    1. 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
    2. 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。
    3. 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
  • 在确定可行函数时,通常,从最佳到最差的顺序如下:
    1. 完全匹配,但常规函数优先于模板。
    2. 提升转换(例如:char和short自动转换为int,float自动转换为double)。
    3. 标准转换(例如:int转换为char,long转换为double)。
    4. 用户定义的转换,如类声明中定义的转换。
  • 在完全匹配时,C++允许某些“无关紧要的转换”
  • 完全匹配允许的无关紧要转换
从实参到形参从实参到形参
TypeType &Typeconst Type
Type &TypeTypevolatitle Type
Type[]* TypeType *const Type
Type (argment-list)Type (*) (argument-list)Type *volatile Type
	struct blot {int a; char b[10];}
	blot int = {25, "spots"};
	...
	recycle(ink);
	//这种情况下,下面所有的原型都是完全匹配的
	void recycle(blot);				// #1 blot to blot
	void recycle(const blot);		// #2 blot to (const blot)
	void recycle(blot &);			// #3 blot to (blot &)
	void recycle(const blot &);	// #4 blot to (const blot &)
  • 大多数情况下,如果有多个匹配的原型,则编译器将无法完成重载 解析过程;如果没有最佳的可行函数,则编译器将生成一条错误消息。
  • 有时候,即使两个函数都完全匹配,仍可完成重载解析。
    1. 指向非const数据的指针和引用优先与非const的指针和引用参数匹配。上例中,如果值定义了#3和#4,则将选择#3。此区别只适用于指针和引用指向的数据。
    2. 其中一个是非模板函数,另一个不是。这种情况下,非模板函数将优先于模板函数(包括显式具体化)。如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。
### LSTM 的深入原理 LSTM 是一种特殊的循环神经网络(RNN),其设计目的是解决传统 RNN 中存在的梯度消失和梯度爆炸问题,从而能够捕捉长时间序列中的依赖关系。LSTM 通过引入门控机制以及细胞状态的概念实现了这一目标。 #### 细胞状态与门控结构 LSTM 的核心在于它的 **细胞状态** 和三个主要的门控单元:遗忘门、输入门和输出门。这些组件共同决定了信息如何流入、存储和流出 LSTM 单元。 1. **细胞状态(Cell State)** - 细胞状态是一个贯穿整个序列的信息流通道,它允许信息在整个序列中传递而不受太多干扰[^1]。 2. **遗忘门(Forget Gate)** - 遗忘门决定哪些信息应该从细胞状态中移除。该门接收当前输入 \(x_t\) 和前一时刻隐藏状态 \(h_{t-1}\),并通过一个 sigmoid 层计算出一个介于 0 到 1 之间的值,表示保留或丢弃的程度。 \[ f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) \] 这里 \(W_f\) 表示权重矩阵,\(b_f\) 表示偏置向量,\(\sigma\) 表示激活函数sigmoid[^1]。 3. **输入门(Input Gate)** - 输入门控制新信息进入细胞状态的程度。这包括两个部分: - 使用另一个 sigmoid 层决定更新哪些部分。 \[ i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) \] - 使用 tanh 层创建一个新的候选值向量,可能加入到状态中。 \[ \tilde{C}_t = \text{tanh}(W_C \cdot [h_{t-1}, x_t] + b_C) \] 4. **细胞状态更新** - 结合遗忘门和输入门的结果,更新细胞状态 \(C_t\)。 \[ C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t \] 其中 \(\odot\) 表示逐元素乘法操作[^1]。 5. **输出门(Output Gate)** - 输出门决定基于新的细胞状态输出什么值。首先通过一个 sigmoid 层确定输出的部分,再通过对细胞状态应用 tanh 函数得到最终输出。 \[ o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) \] \[ h_t = o_t \odot \text{tanh}(C_t) \] #### 实现细节 以下是 Python 中使用 TensorFlow/Keras 构建基本 LSTM 模型的代码示例: ```python import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense # 定义模型架构 model = Sequential() model.add(LSTM(50, activation='relu', input_shape=(n_steps, n_features))) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse') # 打印模型概要 model.summary() ``` 在这个例子中,`LSTM` 层有 50 个单元,`activation='relu'` 设置了激活函数为 ReLU,而 `input_shape` 参数指定了输入数据的时间步数和特征数量[^1]。 尽管 LSTM 能够很好地处理长序列数据,但它也存在一些局限性。例如,在非常长的输入时间步长下,强迫 LSTM 记住单一观测可能会导致性能下降甚至失败[^2]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值