目录
一、函数参数表
二、非引用形参
三、引用形参
四、数组参数-特例说明
五、函数指针作为参数-特例说明
六、不确定形参-特例说明
七、模板形参
八、本文涉及源码
一、函数参数表
函数由函数名以及一组操作数类型唯一地表示。函数的操作数,也即形参,在一对圆括号中声明,形参与形参之间以逗号分隔。每一个函数都有一个相关联的返回类型。
int setval(int val)
{
//函数体
};
这里,定义了一个名为 setval的函数,该函数返回一个 int 型值,并带有一个 int型形参。这个形参就类似于一个局部变量,为函数提供了已命名的局部存储空间,它是在函数的形参表中定义的,并由调用函数时传递函数的实参初始化。形参和实参都是函数参数,只是出现时机不一样:
1)形参:在函数定义()中出现的参数,形参就是拷贝实参的值,随着函数的调用才产生,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放而销毁。
2)实参:在函数调用()中出现的参数(外界实际存在的值),实参可以是常量、变量或表达式,无论实参是何种类型的量,在进行函数调用时,都必须具有确定的值,以便把这些值传送给形参。
函数形参表可以为空,没有任何形参的函数可以用空形参表或含有单个关键字 void 的形参表来表示,但建议采用第二种方式。
void dosomething(); //空参数表,可行
void dosomething(void); //建议
形参表由一系列用逗号分隔的参数类型和(可选的)参数名组成。如果两个参数具有相同的类型,则其类型必须重复声明。参数表中不能出现同名的参数,参数名是可选的,但在函数定义中,通常所有参数都要命名,参数必须在命名后才能使用,否则在函数实现内毫无意义。
int setval(int val_a,int val_b); //正确
int setval(int val_a,val_b); //错误
int setval(int val_a,int val_a); //错误
//无参数名也能通过但参数不能使用,毫无意义
int setval(int)
{
return 0;
};
在函数调用时,对于每一个实参,其类型都必须与对应的形参类型相同,或具有可被转换为该形参类型的类型。编译器在编译时会检查函数的实参。
int setval(int val_a,int val_b)
{
return val_a+val_b;
};
int a = 10;
int b = 11;
setval(a,b); //
setval(a,12); //编译器检查识别为正确参数类型
setval(a,(int)&b);//强制转换了参数类型
setval(a,12.3); //编译器自动转换了参数类型,基础类型向下转换,等于setval(a,12)
setval(a); //参数太少
setval(a,b,10); //参数太多
setval(a,&b); //参数类型不对(int*)
程序运行过程中,在调用函数时,会重新创建该函数所有的形参,若形参具有非引用类型,则复制实参的值,若形参为引用类型,它就只是实参的别名。
二、非引用形参
非引用类型的参数在函数调用时,通过复制对应的实参实现初始化。当用实参副本初始化形参时,函数并没有访问调用所传递的实参本身,因此不会修改实参的值。
int changeVal(int val)
{
val+=5; //该语句仅限于局部参数,不会改变实参本身
return val;
};
int val_arg = 10;
int val_ret = 0;
val_ret=changeVal(val_arg);
printf("%d,%d\n",val_arg,val_ret); //10,15
非引用形参表示对应实参的局部副本。对这类形参的修改仅仅改变了局部副本的值。一旦函数执行结束,这些局部变量就会被释放。
指针作为形参时,和普通变量一样,该指针形参本身与其他非引用类型的形参一样,也仅作用于局部副本。如果函数调用时,将某指针赋给形参,在函数使用的该实参指针的值改变,对外部的指针本身没影响。
void set_ptr(int *pval)
{
pval = NULL;
};
int val = 10;
printf("%0X\n",&val); //61FF0C,本人编辑环境下
set_ptr(&val);
printf("%0X\n",&val); //61FF0C,可见函数只是改变了局部副本,对本身无影响
当然指针作为形参,其指向内容是可以被改变的,因为形参指针虽然是局部副本,但其指向内容和外部指针本身指向内容是一致的。
void set_ptr(int *pval)
{
*pval = 100; //注意先赋值哦
pval = (int*)0X61FF10; //0X61FF0C+0X04,注意根据上一步得到的地址做偏移赋值哦
};
int val = 10;
printf("%0X,%d\n",&val,val); //61FF0C,10
set_ptr(&val);
printf("%0X,%d\n",&val,val); //61FF0C,100
那么如果我们实际项目中为了防止只想函数内局部使用指针,但不想改变指针指向的值呢,答案就是通过const关键字来限制。
void only_set_ptr(const int *pval)
{
*pval = 100; //error,不能给指针指向内容赋值
pval = (int*)0X61FF10; //OK,0X61FF0C+0X04
};
既然谈到const,再延展开了,看看其对基础类型非引用形参的影响,如下文。
int changeVal(int val)
{
val+=5;
return val;
};
int const_Val(const int val)
{
//val += 10; //error
return val+5;
};
const int val_const = 10;
//编译器会做转换,const ->常规,构建一个副本const int _val_const,并用val_const的值在初始化时赋值给_val_const
changeVal(val_const);
int val = 10;
//编译器会做转换,常规 ->const,即函数调用时,构建一个副本const int _val,并用val的值在初始化时赋值给_val
const_Val(val);
因为,const 对象的标准初始化规则是定义时必须初始化。因此,函数调用时,非引用类型的参数初始化复制了实参初始化式的值,实参仍然是以副本的形式传递,无论是const 形参还是非 const 形参,都是复制实参的副本而已,这就是非引用形参特性。
三、引用形参
设想当需要以大型对象作为实参传递时(非引用形参)。对实际的应用而言,复制对象所付出的时间和存储空间代价如何规避,这就要用到引用形参了。对于大型对象、容器、数组等作为参数的情况,或者实参无法复制时,又或者对于需要修改实参时,这些都可以通过引用形参得到解决。
void setVal_realy(int &val)
{
val+=10; //生效咯
};
int val_realy = 10;
printf("val_realy=%d\n",val_realy ); //val_realy=10
setVal_realy(val_realy);
printf("val_realy=%d\n",val_realy ); //val_realy=20
//setVal_realy(10); //error,不能作为左值
int val_realy_b = 20;
//setVal_realy(val_realy + val_realy_b ); //error,同样不能作为左值
与前面所述不同,这次通过&直接关联到其所绑定的真实值,而并非这些对象的副本。定义引用形参时,必须用与该引用绑定的对象初始化该引用,每次调用函数,引用形参被创建并与相应实参关联起来,操作引用参数就相当于操作实参本身。虽然前讲述过,通过传递指针这个非引用形参也能实现对实参的间接访问,但使用引用形参则更安全和更自然。
若既想作为引用形参,又想防止其在函数内部被改变赋值,就用const修饰吧。将不应不修改参数定义为 const 引用,限制函数的对参数的使用,降低异常风险。
void setVal_realy(const int &val)
{
//val+=10; //不生效咯
int in_val = val;
};
//又或
int get_substr_pos(const std::string &str,const char &c)
{
int ret = 0;
while(ret != str.size() && str[ret ] != c)
{
++ret;
}
return ret;
};
in val = 10;
setVal_const(val);
setVal_const(10); //若const不限制,函数内赋值呢
get_substr_pos("abcd",'c');
上述例子不好体现其价值,若是向函数传递大型对象时,需使用引用形参,这就大不同了。虽然复制实参对于内置数据类型的对象或者规模较小的类类型对象来说没有什么问题,但是对于大部分的类类型或者大型数组,它的效率(通常)太低了;使用引用形参,只想函数可以直接访问实参对象,而无须复制它或改变它。
class A
{
public:
A(){
memset(data,0,10000);
};
private:
int data[10000];
};
void do_Something(const A &val)
{
//your code
};
对于那些大型数据,传递引用要不传递实参获得更高的效率。例如,现需要打印一个map容器的内容,通过引用形参或非引用形参都能实现,但如果从效率来看,还是建议采用引用形参吧。
//void print_map(std::map<int,int> maps) //OK,比较与下一句的区别
void print_map(std::map<int,int> &maps)
{
std::map<int,int>::iterator it = maps.begin();
while (it != maps.end())
{
printf("key=%d,val=%d\n",it->first,it->second);
it++;
}
}
//
std::map<int,int> maps_;
maps_[4] = 2;
maps_[1] = 3;
maps_[2] = 4;
print_map(maps_);
针对容器对象作为形参,有时还可以引用 Iterator(迭代器),它是一种Cursor(游标)模式,提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。由iterator提供的方法访问聚合对象中的各个元素,达到引用形参的效果。
void print_map(std::map<int,int>::iterator begin,std::map<int,int>::iterator end)
{
std::map<int,int>::iterator it = begin;
while (it != end)
{
printf("key=%d,val=%d\n",it->first,it->second);
it++;
}
}
std::map<int,int> maps_;
maps_[4] = 2;
maps_[1] = 3;
maps_[2] = 4;
print_map(maps_.begin(),maps_.end());
四、数组参数-特例说明
前面我们提到,有些对象是不允许复制的,例如数组,组不能复制,所以数组名在作为参数时,会被编译器转换,数组名会自动转化为指向其第一个元素的指针。但是仅传递了首元素的指针,长度就是一个不可忽略的痛点了,毕竟数组需要知道长度才好处理信息,最好能显式传递数组大小。
void print_group(int g[]);
void print_group(int g[10]);
//以上编译器都能通过, 但建议采用下列方式,就像我们main函数一样int main(int argc, char* argv[])
void print_group(int g[],int size);
数组以普通的非引用类型传递,此时数组会悄悄地转换为指针。在传递数组时,形参复制的是指向数组第一个元素的指针的值,而不是数组元素本身。函数操纵的是指针的副本,因此不会修改实参指针的值,但可通过该指针改变它所指向的数组元素的值来修改数组元素本身。
//等同于指针类型
void print_group(int *g);
void print_group(int *g,int size);
不需要修改数组形参的元素时,函数应该将形参定义为指向 const 对象的指针
//等同于指针类型
void print_group(const int *g);
void print_group(const int *g,int size);
如果数组想通过引用传递给函数呢,就需要指定传递数组的引用本身,这样编译器不会将数组实参转化为指针。
void print_group(int g[],int size)
{
for(int i=0; i<size; i++)
{
printf("%d ",g[i]);
};
printf("\n");
};
void print_group(int (&g)[10])
{
for(int i=0; i<10; i++)
{
printf("%d ",g[i]);
};
printf("\n");
};
int gval[9] = {1,2,3,4,5,6,7,8,9};
print_group(gval,9);
print_group(gval); //error
int gval10[10] = {0,1,2,3,4,5,6,7,8,9};
print_group(gval10,9) //OK
print_group(gval10); //OK
函数二只严格地接受含有 10 个 int 型数值的数组,这限制了哪些数组可以传递。&arr 两边的圆括号是必需的,因为下标操作符具有更高的优先级。
对于多维数组作为参数来说,多维数组的元素本身就是数组。除了第一维以外的所有维的长度都是元素类型的一部分,必须明确指定,即:
void print_mgroup(int gs[][2],int First_dimension_size);
void print_mgroup(int (*gs)[2],int First_dimension_size);
//三维
void print_mgroup(int gs[][2][3],int First_dimension_size);
void print_mgroup(int (*gs)[2][3],int First_dimension_size);
五、函数指针作为参数-特例说明
函数指针也是指针,只是一般的指针指向变量、对象、数组等,函数指针指向的是函数入口地址,其作为参数时,和普通指针作为参数是类似的。
int myFunc(int val)
{
return val+10;
}
void print_funcRVal(int (*pfunc)(int val))
{
printf("%d \n",pfunc(10));
}
//
int (*pf)(int val) = &myFunc;
print_funcRVal(pf); //输出20
当然,函数指针参数有事会很复杂,函数指针通过typedef重定义一下,增加代码阅读性。
typedef int (*pfunc)(int val);
pfunc pf_ = &myFunc;
print_funcRVal(pf);
六、不确定形参-特例说明
c++中printf系列函数都是不确定形参的典型应用代表:
int printf(const char *format, ...);
其省略符形参就是不确定形参,实际是使用了 varargs 实现的,例如。
void out_print(const char* lpFormat, ...)
{
va_list args;
char szBuffer[256] = {0};
va_start(args, lpFormat);
vsnprintf(szBuffer, sizeof(szBuffer), lpFormat, args);
va_end(args);
std::cout << szBuffer << std::endl;
}
out_print("hello %s","world"); //输出:hello world
在无法列举出传递给函数的所有实参的类型和数目时,可以使用省略符形参。省略符暂停了类型检查机制。它们的出现告知编译器,当调用函数时,可以有 0 或多个实参,而实参的类型未知。注意,省略符号只能放置在最后面。
void out_print( ...); //OK
void out_print(const char* lpFormat, ...); //OK
void out_print(const char* lpFormat, int val, ...); //OK
void out_print(const char* lpFormat, ..., int val); //error
void out_print( ..., const char* lpFormat); //error
大部分带有省略符形参的函数都利用显式声明的参数中的一些信息,来获取函数调用中提供的其他可选实参的类型和数目。因此向第一种形式的函数声明是不建议的。
七、模板形参
函数模板是一个独立于类型的函数,通过指定类型,产生函数的特定类型版本匹配具体场景应用。模板定义以关键字 template 开始,后接模板形参表,模板形参表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔,模板形参表不能为空。
//template <class T> //class 和 typename 没有区别
template <typename T>
T max(const T &v1, const T &v2)
{
return (v1>v2)?v1:v2;
};
模板形参表类似于函数普通形参表,函数形参表定义了特定类型的局部变量但并不初始化那些变量,在运行时再提供实参来初始化形参 。在上述代码中,max函数声明一个名为 T 的类型形参。在 max内部,可以使用名字T引用一个类型,T 表示哪个实际类型由编译器根据所用的函数而确定。
模板函数在调用时,编译器会根据传入实参进行检测,编译器会推断哪个(或哪些)模板实参绑定到模板形参。一旦编译器确定了实际的模板实参,就称它实例化了函数模板的一个实例。实质上,编译器将确定用什么类型代替每个类型形参,以及用什么值代替每个非类型形参。推导出实际模板实参后,编译器使用实参代替相应的模板形参产生编译该版本的函数。
max<int>(14,16);
//编译器实质类似于
int max_int(const int &v1, const int &v2)
{
return (v1>v2)?v1:v2;
};
max_int(14,16);
虽然编译器会推断哪个(或哪些)模板实参绑定到模板形参,但模板函数调用时,建议开发者显式指定类型。
std::cout << max(14,16) << std::endl; //OK
std::cout << max<int>(14,16) << std::endl; //OK,建议
std::cout << max<std::string>(("nihao"),("hi")) << std::endl; //显式指定
//std::cout << max("nihao","hi") << std::endl; //error,编译器无法明确
//std::cout << max(14,16.5) << std::endl; //error
std::cout << max<float>(14,16.5) << std::endl; //OK,显式指定类型一被隐秘转换为float
八、本文涉及源码
构建test.cpp源文件,编译命令g++ test.cpp -o test.exe测试如下:
test.cpp源文件
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <iostream>
#include <map>
//可行但参数无意义
int setval01(int)
{
return 0;
};
int setval(int val_a,int val_b)
{
return val_a+val_b;
};
int changeVal(int val)
{
val+=5;
return val;
};
int const_Val(const int val)
{
//val += 10; //error
return val+5;
};
//
void set_ptr(int *pval)
{
*pval = 100; //注意先赋值哦
pval = (int*)0X61FF10; //0X61FF0C+0X04
};
void only_set_ptr(const int *pval)
{
//*pval = 100; //error,不能给指针指向内容赋值
pval = (int*)0X61FF10; //OK,0X61FF0C+0X04
};
//
void setVal_realy(int &val)
{
val+=10; //生效咯
};
void setVal_const(const int &val)
{
//val+=10; //不生效咯
int in_val = val;
};
int get_substr_pos(const std::string &str,const char &c)
{
int ret = 0;
while(ret != str.size() && str[ret ] != c)
{
++ret;
}
return ret;
};
class A
{
public:
A(){
memset(data,0,10000);
};
private:
int data[10000];
};
void do_Something(const A &val)
{
//your code
};
//void print_map(std::map<int,int> maps) //OK,比较与下一句的区别
void print_map(std::map<int,int> &maps)
{
std::map<int,int>::iterator it = maps.begin();
while (it != maps.end())
{
printf("key=%d,val=%d\n",it->first,it->second);
it++;
}
}
void print_map(std::map<int,int>::iterator begin,std::map<int,int>::iterator end)
{
std::map<int,int>::iterator it = begin;
while (it != end)
{
printf("key=%d,val=%d\n",it->first,it->second);
it++;
}
}
void print_group(int g[],int size)
{
for(int i=0; i<size; i++)
{
printf("%d ",g[i]);
};
printf("\n");
};
void print_group(int (&g)[10])
{
for(int i=0; i<10; i++)
{
printf("%d ",g[i]);
};
printf("\n");
};
//void print_mgroup(int gs[][2],int row_size)
void print_mgroup(int (*gs)[2],int row_size)
{
for(int i=0; i<row_size; i++)
{
for(int j=0; j<2; j++)
printf("%d ",gs[i][j]);
};
printf("\n");
};
//void print_m_group(int gs[][2][3],int First_dimension_size);
//void print_m_group(int (*gs)[2][3],int First_dimension_size);
int myFunc(int val)
{
return val+10;
}
void print_funcRVal(int (*pfunc)(int val))
{
printf("%d \n",pfunc(10));
}
#ifdef WIN32
#define vsnprintf _vsnprintf
#endif
void out_print(const char* lpFormat, ...)
{
va_list args;
char szBuffer[256] = {0};
va_start(args, lpFormat);
vsnprintf(szBuffer, sizeof(szBuffer), lpFormat, args);
va_end(args);
std::cout << szBuffer << std::endl;
}
//template <class T> //class 和 typename 没有区别
template <typename T>
T max(const T &v1, const T &v2)
{
return (v1>v2)?v1:v2;
};
int main(int argc, char* argv[])
{
int a = 10;
int b = 11;
setval(a,b);
setval(a,12); //
setval(a,(int)&b);//转换类型
setval(a,12.0); //参数类型不对
//setval(a); //参数太少
//setval(a,b,10); //参数太多
int val_arg = 10;
int val_ret = 0;
val_ret=changeVal(val_arg);
printf("%d,%d\n",val_arg,val_ret);
int val = 10;
printf("%0X,%d\n",&val,val); //61FF0C
set_ptr(&val);
printf("%0X,%d\n",&val,val); //61FF0C
const int val_const = 10;
changeVal(val_const);
const_Val(val);
//
int val_realy = 10;
printf("val_realy=%d\n",val_realy ); //val_realy=10
setVal_realy(val_realy);
printf("val_realy=%d\n",val_realy ); //val_realy=20
setVal_const(10);
get_substr_pos("abcd",'c');
//
std::map<int,int> maps_;
maps_[4] = 2;
maps_[1] = 3;
maps_[2] = 4;
print_map(maps_);
print_map(maps_.begin(),maps_.end());
//
int gval[9] = {1,2,3,4,5,6,7,8,9};
print_group(gval,9);
//print_group(gval); //error
int gval10[10] = {0,1,2,3,4,5,6,7,8,9};
print_group(gval10,9); //OK
print_group(gval10); //OK
//
int gs[2][2] = {{1,2},{3,4}};
print_mgroup(gs,2); //OK
//
int (*pf)(int val) = &myFunc;
print_funcRVal(pf);
typedef int (*pfunc)(int val);
pfunc pf_ = &myFunc;
print_funcRVal(pf);
//
out_print("hello %s","world");
//
std::cout << max(14,16) << std::endl; //OK
std::cout << max<int>(14,16) << std::endl; //OK,建议
std::cout << max<std::string>(("nihao"),("hi")) << std::endl; //显式指定
//std::cout << max("nihao","hi") << std::endl; //error,编译器无法明确
//std::cout << max(14,16.5) << std::endl; //error
std::cout << max<float>(14,16.5) << std::endl;
return 0;
}