C语言基础:函数(定义函数、声明函数、调用函数(传值调用 / 引用调用)、函数的参数(形参 / 实参))
1. 函数
函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数,即主函数 main() ,所有简单的程序都可以定义其他额外的函数。
您可以把代码划分到不同的函数中。如何划分代码到不同的函数中是由您来决定的,但在逻辑上,划分通常是根据每个函数执行一个特定的任务来进行的。
函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。
C 标准库提供了大量的程序可以调用的内置函数。例如,函数 strcat() 用来连接两个字符串,函数 memcpy() 用来复制内存到另一个位置。
1.1 定义函数
返回类型 函数名称( 参数 )
{
函数体
}
在 C 语言中,函数由一个函数头和一个函数主体组成。下面列出一个函数的所有组成部分:
返回类型:一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字 void。
函数名称:这是函数的实际名称。函数名和参数列表一起构成了函数签名。
参数:参数就像是占位符。当函数被调用时,您向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
函数体:函数体包含一组定义函数执行任务的语句。
max函数有两个参数 num1 和 num2,会返回这两个数中较大的那个数:
int main () {
printf("max(3,4) = %d",max(3,4));
}
/* 函数返回两个数中较大的那个数 */
int max(int num1, int num2) {
/* 局部变量声明 */
int result;
if (num1 > num2)
result = num1;
else
result = num2;
return result;
}
运行结果: max(3,4) = 4;
1.2 函数声明
函数声明会告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。
当您在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,您应该在调用函数的文件顶部声明函数。
函数声明包括以下几个部分:
return_type function_name( parameter list );
针对上面定义的函数 max(),以下是函数声明:
int max(int num1, int num2);
在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明:
int max(int, int);
1.3 调用函数
当调用函数时,有两种向函数传递参数的方式:
1.3.1 传值调用
向函数传递参数的传值调用方法,把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
默认情况下,C 语言使用传值调用方法来传递参数。一般来说,这意味着函数内的代码不会改变用于调用函数的实际参数。函数 swap() 定义如下:
void swap(int x, int y)
{
int temp;
temp = x;
x = y;
y = temp;
return;
}
现在,让我们通过传递实际参数来调用函数 swap():
#include <stdio.h>
void swap(int x, int y);
int main ()
{
int a = 100;
int b = 200;
printf("交换前,a 的值: %d\n", a );
printf("交换前,b 的值: %d\n", b );
swap(a, b);
printf("交换后,a 的值: %d\n", a );
printf("交换后,b 的值: %d\n", b );
return 0;
}
void swap(int x, int y)
{
int temp;
temp = x;
x = y;
y = temp;
return;
}
运行结果为:
1.3.2 引用调用
通过引用传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。
传递指针可以让多个函数访问指针所引用的对象,而不用把对象声明为全局可访问。
void swap(int *x, int *y)
{
int temp;
temp = *x;
*x = *y;
*y = temp;
return;
}
现在,让我们通过引用传值来调用函数 swap():
#include <stdio.h>
void swap(int *x, int *y);
int main () {
int a = 100;
int b = 200;
printf("交换前,a 的值: %d\n", a );
printf("交换前,b 的值: %d\n", b );
swap(&a, &b);
printf("交换后,a 的值: %d\n", a );
printf("交换后,b 的值: %d\n", b );
return 0;
}
void swap(int *x, int *y) {
int temp;
temp = *x;
*x = *y;
*y = temp;
return;
}
1.4 函数参数
1.4.1 形参
形参就是定义函数时的那个参数。
1.4.2 实参
实参就是调用函数时传递的那个参数。
1.5函数调用
#include <stdio.h>
#include <stdlib.h>
void fun(int a, int b);
int main()
{
int a, b;
scanf("%d %d", &a, &b);
fun(a, b);
return 0;
}
void fun(int a, int b){
int res = a + b;
printf("res = %d", res);
}
C++
为了支持分离式编译,C++将定义和声明区分开。其中声明规定了变量的类型和名字,定义除此功能外还会申请存储空间并可能为变量赋一个初始值。
extern
如果想声明一个变量而非定义它,就使用关键字extern并且不要显式地初始化变量:
extern int i;
extern int i = 1;
static
当我们在C/C++用static修饰变量或函数时,主要有三种用途:
局部静态变量
外部静态变量/函数
类内静态数据成员/成员函数
其中第三种只有C++中有,我们后续在面向对象程序设计中再探讨,这里只讨论静态局部/全局变量。
1. 静态局部变量
在局部变量前面加上static说明符就构成静态局部变量,例如:
static int a;
static int array[5] = {1, 2, 3, 4, 5};
静态局部变量在函数内定义,但不像自动变量那样当函数被调用时就存在,调用结束就消失,静态变量的生存期为整个源程序
静态变量的生存期虽然为整个源程序,但是作用域与自动变量相同,即只能在定义该变量的函数内使用该变量,退出函数后虽然变量还存在,但不能够使用它
对基本类型的静态局部变量如果在声明时未赋初始值,则系统自动赋0值;而对普通局部变量不赋初始值,那么它的值是不确定的
根据静态局部变量的特点,它的生存期为整个源程序,在离开定义它的函数(作用域)但再次调用定义它的函数时,它又可继续使用,而且保存了前次被调用后留下的值。因此,当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量,虽然用全局变量也可以达到上述目的,但全局变量有时会造成意外的副作用,因此最好采用局部静态变量。例如:
#include <iostream>
void foo() {
int j = 0;
static int k = 0;
++j;
++k;
printf("j:%d, k:%d\n", j, k);
}
int main(void)
{
for (int i = 1; i <= 5; i++) {
foo();
}
}
j:1, k:1
j:1, k:2
j:1, k:3
j:1, k:4
j:1, k:5
2. 静态全局变量(C++废弃,用匿名命名空间替代)
Tips:对于全局变量,不管是否被static修饰,它的存储区域都是在静态存储区,生存期为整个源程序。只不过加上static后限制这个全局变量的作用域只能在定义该变量的源文件内。
全局变量(外部变量)的声明之前加上static就构成了静态的全局变量,全局变量本身就是静态存储变量,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同,这两者的区别在于非静态全局变量的作用域是整个源程序。当一个源程序由多个源程序组成时,非静态的全局变量在各个源文件中都是有效的,而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其他源文件中不能使用它。
这种在文件中进行静态声明的做法是从C语言继承而来的,在C语言中声明为static的全局变量在其所在的文件外不可见。这种做法已经被C++标准取消了,现在的替代做法是使用匿名命名空间。
匿名命名空间:指关键字namespace后紧跟花括号括起来的一系列声明语句,具有如下特点:
在匿名命名空间内定义的变量具有静态生命周期
匿名空间在某个给定的文件内可以不连续,但是不能跨越多个文件
每个文件定义自己的匿名命名空间,不同文件匿名命名空间中定义的名字对应不同实体
如果在一个头文件中定义了匿名命名空间,则该命名空间内定义的名字在每个包含该头文件的文件中对应不同实体
namespace {
int i;
}
3. 总结
static这个说明符在不同地方所起的作用域是不同的,比如把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期,把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。
auto
- C++98中auto用法(C++11已废弃)
C++98 auto用于声明变量为自动变量(拥有自动的生命周期),C++11已经删除了该用法,取而代之的是“变量的自动类型推断方法”。
int a = 10;
auto int b = 20;
static int c = 30;
C++11新标准引入了auto类型说明符,让编译器通过初始值来自动推断变量类型(这意味着通过auto定义的变量必须有初始值)。
int a = 10;
auto auto_a = a;
- auto会去除变量的引用语义
当引用对象作为初始值时,真正参与初始化的是引用对象的值,此时编译器会以引用对象的类型作为auto推算的类型:
int main(void) {
int i = 10;
int &ri = i;
auto auto_i = ri;
}
如果希望推断出来的auto类型包含引用语义,我们需要用&明确指出:
int main(void) {
int i = 10;
auto &auto_i = i;
}
- auto忽略顶层const
auto一般会忽略掉顶层const,同时底层const会被保留下来:
int main(void) {
const int ci = 10;
auto auto_ci = ci;
auto_ci = 20;
const int &cr = ci;
auto auto_cr = cr;
auto_cr = 20;
const int *cp = &ci;
auto auto_cp = cp;
}
如果希望推断出来的auto类型是一个顶层const,我们需要通过const关键字明确指出:
int main(void) {
const int ci = 10;
const auto auto_ci = ci;
}
const
有时我们希望定义一个不能被改变值的变量,可以使用关键字const对变量类型加以限定。
1. const对象必须初始化
因为const对象一经创建后其值就不能再改变,所以const对象必须初始化,但是初始值可以是任意复杂的表达式:
const int i = get_size();
const int j = 42;
const int k;
- 默认情况下const仅在文件内有效
举个例子,我们在编译时初始化一个const对象:
const int i = 10;
编译器会在编译过程把用到该变量的地方都替换为对应的值。为了执行这个替换,编译器必须知道变量的初始值,如果程序包含多个文件,那么每个用了这个const对象的文件都必须得能访问到它的初始值才行(即每个文件都要定义const对象)。为了避免对同一变量的重复定义,当多个文件中出现同名的const对象时,其实等同于在不同文件中分别定义了独立的变量。
const int i = 10;
const int i = 5;
如果想在多个文件之间共享const对象,那么必须在变量的定义之前添加extern关键字:
extern const int i = 10;
#include <iostream>
int main(void) {
extern int i;
std::cout << "i:" << i << std::endl;
}
- 允许常量引用绑定非常量对象、字面值甚至一般表达式
一般而言,引用的类型必须与其所引用对象的类型一致,但是有两个例外:
初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用类型即可,允许为一个常量引用绑定非常量的对象、字面值甚至是一个一般表达式(如下)
可以将基类的指针或引用绑定到派生类对象上(后续面向对象章节再探讨)
int i = 10;
const int &ri1 = i;
const int &ri2 = 100;
const int &ri3 = 1 + 1;
- 顶层const与底层const
指针本身是一个对象,因此指针本身是不是常量与指针所指对象是不是常量是两个独立的问题,前者被称为顶层const,后者被称为底层const。
Tips:指针类型既可以是顶层const也可以是底层const,其他类型要么是顶层常量要么是底层常量。
顶层const用于表示任意的对象是常量,包括算数类型、类和指针等,底层const用于表示引用和指针等复合类型的基本类型部分是否是常量。
int i = 10;
int *const p1 = &i;
const int *p2 = &i;
const int *const p3 = &i;
const int &r1 = i;
constexpr
C++11引入了常量表达式constexpr的概念,指的是值不会改变并且在编译期间就能得到计算结果的表达式。
const int i = 10;
const int j = i + 1;
const int k = size();
表达式, 运行时才能获得具体值就不是常量表达式
在一个复杂系统中,我们很难分辨一个初始值是否是常量表达式,通过constexpr关键字声明一个变量,我们可以让编译器来验证变量的值是否是一个常量表达式。
- 字面值是常量表达式
算术类型、引用和指针都属于字面值类型,自定义类则不属于字面值类型,因此也无法被定义为constexpr。
Tips:尽管指针和引用都能被定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr、0或者是存储于某个固定地址中的对象。
- constexpr是对指针的限制
在constexpr声明中定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关:
const int *pi1 = nullptr;
constexpr int *pi2 = nullptr;
我们也可以让constexpr指针指向常量:
constexpr int i = 10;
constexpr const int *pi = &i;
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)