第六章 函数设计
函数接口的两个要素是参数和返回值。C 语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。C++ 语言中多了引用传递(pass by reference)。
6.1 参数设计
一般地,应将目的参数放在前面,源参数放在后面。
void StringCopy(char *strDestination, char *strSource);
如果参数是指针,且仅作输入用,则应在类型前加 const
,以防止该指针在函数体内被意外修改。
void StringCopy(char *strDestination,const char *strSource);
如果输入参数以值传递的方式传递对象,则宜改用const &
方式 来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
6.2 返回值设计
不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而 错误标志用 return 语句返回。
char c;
c = getchar();
if (c == EOF)//error
int getchar(void);
//在正常情况下,getchar 的确返回单个字符。但如果 getchar 碰到文件结束标志或发生读错误,它必须返回一个标志 EOF。为了区别于正常的字符,只好将 EOF 定义为负数(通常为负 1)。因此函数 getchar 就成了 int 类型
引用传递与值传递
如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传 递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会 出错。
class String
{ …
// 赋值函数
String & operate=(const String &other);
// 相加函数,如果没有 friend 修饰则只许有一个右侧参数
friend String operate+( const String &s1, const String &s2);
private:
char *m_data;
}
String & String::operate=(const String &other)
{
if (this == &other)
return *this;
delete m_data;
m_data = new char[strlen(other.data)+1];
strcpy(m_data, other.data);
return *this; // 返回的是 *this 的引用,无需拷贝过程
//如果用“值传递”的方式,虽然功能仍然正确,但由于 return 语句要把 *this 拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,降低了赋值函数的效率
}
String a,b,c;
…
a = b; // 如果用“值传递”,将产生一次 *this 拷贝
a = b = c; // 如果用“值传递”,将产生两次 *this 拷贝
String 的相加函数 operate + 的实现如下:
String operate+(const String &s1, const String &s2)
{
String temp;
delete temp.data; // temp.data 是仅含‘\0’的字符串
temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];
strcpy(temp.data, s1.data);
strcat(temp.data, s2.data);
return temp;
}
//对于相加函数,应当用“值传递”的方式返回 String 对象。如果改用“引用传递”,那么函数返回值是一个指向局部对象 temp 的“引用”。由于 temp 在函数结束时被自动销毁,将导致返回的“引用”无效。
6.3 函数内部的实现
在函数体的“入口处”,对参数的有效性进行检查(使用断言assert
);在函数体的“出口处”,对 return 语句的正确性和效率进行检查。
return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
char * Func(void)
{
char str[] = “hello world”; // str 的内存位于栈上
…
return str; //error
}
如果函数返回值是一个对象,要考虑 return 语句的效率。
//这是临时对象的语法,表示“创建一个临时对象并返回它”。
return String(s1 + s2);
//“先创建一个局部对象 temp 并返回它的结果”
String temp(s1 + s2);
return temp;
实质不然,上述代码将发生三件事:
首先,temp 对象被创建,同时完成初始化;
然后拷贝构造函数把 temp 拷贝到保存返回值的外部存储单元中;
最后,temp 在函数结束时被销毁(调用析构函数)。
然而“创建一个临时对象并返回它”的过程是不同的,编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率。
类似地,不要将
return int(x + y); // 创建一个临时变量并返回它
写成
int temp = x + y;
return temp;
由于内部数据类型如 int,float,double 的变量不存在构造函数与析构函数,虽然该“临时变量的语法”不会提高多少效率,但是程序更加简洁易读。
6.5 使用断言
程序一般分为 Debug 版本和 Release 版本,Debug 版本用于内部调试,Release 版本 发行给用户使用。 断言 assert 是仅在 Debug 版本起作用的宏,它用于检查“不应该”发生的情况。
assert 不是函数,而是宏。
#include "assert.h"
#ifdef NDEBUG //一旦定义了NDEBUG宏,assert() 就无效了,运用于发布模式
#define assert(e) ((void)0)
#else
#define assert(e) \
((void) ((e) ? ((void)0) : __assert (#e, __FILE__, __LINE__)))
#endif
void assert( int expression );//无返回值
assert() 会对表达式expression进行检测:
- 如果expression的结果为 0(条件不成立),那么断言失败,表明程序出错,assert() 会向标准输出设备(一般是显示器)打印一条错误信息,并调用 abort() 函数终止程序的执行。
- 如果expression的结果为非 0(条件成立),那么断言成功,表明程序正确,assert() 不进行任何操作。
#define NDEBUG //在发布模式中添加以使assert()失效
#include <stdio.h>
#include <assert.h>
int main(){
int m, n, result;
scanf("%d %d", &m, &n);
assert(n);//分母不为零
result = m / n;
printf("result = %d\n", result);
return 0;
}
6.6 引用与指针
https://blog.csdn.net/qq_51556066/article/details/129676701?spm=1001.2014.3001.5501
const修饰指向与变量本身
int main()
{
const int n=5;//cpp中,n为一个有名字的常变量
int ar[n]={1,2,3,4,5}; //在.C的编译环境下无法通过,n不是常量,#define enum
int b=0;
int *p=(int*)&n;
*p=100;
b=n; //n=5,在编译过程中已经拿5替换了n,此处应为b=5
printf("n=%d b=%d *p=%d \n",n,b,*p);
return 0;
}
C编译方式与C++编译方式对于const(常性)的解释不同。
int main()
{
int a=10,b=20;
int * p1=&a;
//*p1=100;a=100;p1=&b;
const int* p2=&a;
//int const* p2=&a;与上一行写法等价
//*p2=100; error
p2=&b;
//const在*左边,修饰指针的指向能力,所以不能通过*p2改变a的值,但是p2本身可以被改变
int* const p3=&a;
//const在*右边,修饰指针变量名,使p3成为一个常性指针
*p3=100;
//p3=&b; error
const int* const p4=&a;//指针p4的本身和指向都被const修饰为常性,不可修改
//*p4=100; error
//p4=&b; error
}
int main()
{
int a=10;
const int b=20;
int *p1=&a;
//int* p2 = &b; error //用普通指针指向常变量,可以通过*p2=200改变b;
const int* p2 = &b;
int* p3=(int*)b;//不安全,二义性
*p3=100;
//常变量应该拿指向为常性的指针来指向其地址const int*,不能同普通指针来指向。
}
指针之间的赋值,数值兼容性
//指针之间的相互赋值
//修改能力不能被扩展
int main()
{
const int a=10; //常变量,不允许被改变
const int* s=&a; //s指向为常性,常性指针可以指向常变量or普通变量,只是不能通过解引用改变变量的值
int* p1=s; //error 可以通过*p1改变a的值,本来并不能通过*s改变a的值
const int* p2=s;
int* const p3=s; //error const修饰p3自身不能改变,但可以通过解引用*p3修改a
const int* const p4=s;
return 0;
}
int main()
{
int a=10,b=10;
int* const s=&a; //s本身不能被改编,但可以通过解引用*s改变指向
int *p=s; //不改变s
const int *p2=s; //可以指向但不能改变
int * const p3=s;
const int * const p4=s;
return 0;
}
引用&(别名)
int main()
{
int a=10;
int b=a;
int& c=a;//类型&变量-->引用;&变量-->地址
a+=10;
cout<<"a="<<a<<"c"<<c<<endl;
c+=10;
cout<<"a="<<a<<"c"<<c<<endl;
cout<<"&a: "<<&a<<endl;
cout<<"&c: "<<&a<<endl;
//a和c的地址相同
return 0;
}
引用的特点
- 定义引用必须初始化
- 没有空引用
- 没有引用的引用(int&& x),不能一次第一所谓的二级引用
int main()
{
int a=10,b=20;
//int& x; error 定义引用必须初始化,没有空引用
//int&& x=a; error 不能一次定义二级引用
int& x=a;
int& y=x;
return 0;
}
引用作为形参相较于指针的区别
void Swap_Int(int* ap,int* bp)
{
assert(ap!=NULL && bp!=NULL);
int tmp=*ap;
*ap=bp;
*bp=tmp;
}
void Swap(int& x,int& y)
{
//没有空引用,不需要进行assert断言或者判空
int tmp=x;
x=y;
y=tmp;
}b
int main()
{
int a=10,b=20;
cout<<"a="<<a<<"b="<<b<<endl;
//Swap_Int(&a,&b);
Swap(a,b);
cout<<"a="<<a<<"b="<<b<<endl;
return 0;
}
引用与const的关系
int main()
{
int a=10;
int& b=a;
b+=10;
int const& c=a; //cosnt修饰&,常引用,c可以读取a的值,但不能通过修改c来修改a值
int& const x=a; //const修饰引用x本身,系统会忽略此const,可以读取和修改x
cout<<c<<endl;
//c+=100; error
}
int main()
{
const int a=10;
//int& x=a; error 常变量不能被修改,利用普通引用则可以通过引用x来修改a
const int& y=a;
}
函数形参设计
void fun_a(int x)
{}
void fun_b(int& y)
{}
void fun(const int& y)
{}
常引用
int main()
{
int a=10;
const int b=20;
const int& rx=a;
const int& ry=b;
const int& rz=100;
//tmp=100;
//const int& rz=tmp;
}
左值&右值&右值引用
//左值left value,可以进行取地址操作&a,与类型无关
//右值right value,不可以进行取地址操作,eg:10,&10 error
int main()
{
int a=10; //&a,left value
const int b=20; //&b,left value
int& x=a; //&x,left value
const int& y=b; //&y,lvalue
double dx=12.23; //&dx,lvalue
int ar[5]{1,2,3,4,5}; //&ar[1],lvalue
//const int& r=b;
const int& z=10;//常引用才能作为常变量的别名
//int tmp=10; 引用字面常量时需要开辟一个临时变量,此引用z时临时变量tmp的别名
//const int& z=tmp;
//z+=10; error z为常引用不能修改原值
//int&& ra=a; error,a为左值不能进行右值引用
int&& r=10; //右值引用,r变成10的别名
//int tmp=10;
//int& r=tmp;
r+=10; //可以进行运算,但不是对字面常量10进行运算,而是对临时变量tmp做运算
return 0;
}
其他引用形式
//数组的引用
int main()
{
int ar[5]{1,2,3,4,5}; //&ar[1],lvaule
int& a=ar[1]; //引用数组里的某个元素
//int &br[5]; error 数组开辟空间,引用&不开辟空间,矛盾
int (&cr)[5]=ar; //引用整个数组,cr是ar的别名
cout<<"sizeof(ar):"<<sizeof(ar)<<endl;//20
cout<<"sizeof(cr):"<<sizeof(cr)<<endl;//20
}
//指针类型的引用
int main()
{
int a=10,b=20;
int* ip=&a;
int& x=a;
int* s=ip;
int *&ps=ip; //别名ps引用整形指针ip(int*代表整型指针类型)
*ps=100; //等价于*ip=100
cout<<"*ps=>"<<*ps<<"*ip=>"<<*ip<<endl;
ps=&b; //等价于ip=&b
cout<<"*ps=>"<<*ps<<"*ip=>"<<*ip<<endl;
*ps=200;
cout<<"*ps=>"<<*ps<<"*ip=>"<<*ip<<endl;
return 0;
}
引用和指针的区别(重点)
引用是指针的语法糖
语法规则上的区别
- 从语法规则上将,指针变量存储某个实例(变量或对象)的地址;引用则是某个实例的别名。
- 程序为指针变量分配内存区域(32位4字节x86,64位8字节x64);而不为引用分配内存区域。
- 在使用指针和引用所指向或别名的实例时,解引用是指针使用时要在前加“*”;引用可以直接使用。
- 指针变量的值可以发生改变,存储不同实例的地址(修改所指向的地址,eg:指针++or指针–);引用在定义时就别初始化,之后无法改变(不能再是其他实例的引用)。
- 指针变量可以为空(空指针NULL);没有空引用。eg:形参的区别,形参位指针变量则需要判空或者断言,形参为引用则无需此操作。
- 指针变量作为形参时需要测试它的合法性(判空NULL);引用不需要判空,则更加安全。
- 对指针变量使用“”sizeof“得到的时指针变量的大小(32位4字节x86,64位8字节x64);对引用变量使用”sizeof“得到的大小是所引用变量的大小。
- 理论上指针的级数没有限制;但引用只有一级。即不存在引用的引用,但可以有指针的指针。区别:int && a;右值引用。
- 对指针变量的操作,会使指针指向下一个实体(变量或对象)的地址,而不知改变所指实体(变量或对象)的内容。对引用的操作直接反映到所引用的实体(变量或对象)。eg:++引用与++指针的效果不一样。
- 补充注意点:函数不允许将局部变量以引用或者指针的形式返回。
int funa(int* p)
{
if(p==NULL) //指针需要判空or断言
{
return -1;
}
*p += 10;
return *p;
}
int funb(const int& a)
{
}
int main()
{
int a=10;
int x=10;
int& b=a;
b=x; //4是把x赋值给a
int=* ip=&a;
*ip =100;
b+=200;
funb(10);
return 0;
}
//指针和引用的大小,sizeof的使用
int main()
{
double dx=12.25;
double& rx=dx;
double* dp=&dx; //double *dp=℞
cout<<"sizeof(rx):"<<sizeof(rx)<<endl;
cout<<"sizeof(dp):"<<sizeof(rx)<<endl;
retrun 0;
}
//++引用与++指针的效果不一样
//++指针,指向下一个储存单元地址;++引用,直接修改所引用对象的值
int main()
{
int ar[5]{1,2,3,4,5};
int& a=ar[0];
int* p=&ar[0];
for(int i=0;i<5;++i)
{
cout<<ar[i]<<" ";
}
cout<<endl;
//1 2 3 4 5
++a; //ar[0]的值+1
a+=10' //ar[0]的值再+10
for(int i=0;i<5;++i)
{
cout<<ar[i]<<" ";
}
//12 2 3 4 5
cout<<endl;
cout<<"*p "<<*p<<endl; //*p 12
++p; //p从指向ar[0]改为指向ar[1]
cout<<"*p "<<*p<<endl; //*p 2
return 0;
}
//引用与指针的相同点,函数不允许将局部变量以引用或者指针的形式返回(静态变量、全局变量可以:在函数结束后生存期未结束or形参也为引用,接受变量也为引用)
int& funx(int& y)
{
a+=20;
return a;
}
int* funa()
{
int a=10;
retrun &a;
}
int& funb()
{
int a=20;
return a;
}
void fun()
{
int ar[10]={1};
}
int main()
{
int *p=funa();
fun(); //清理了调用funa()时所分配的栈空间,原有值被覆盖
cout<<*p<<endl; //0
int& x=funb();
fun();
cout<<x<<endl; //0
int& x=funx(y);
return 0;
}
汇编层面的区别
//从底层上讲,引用是一个自身为常性的指针
void fun(int& x)
{
int* ip=&x;
x=100;
}
int main()
{
int a=10;
int& b=a;
fun(a);
fun(b);
return 0;
}
//指针替换引用后
void fun(int * const x)
{
int* ip=x;
*x=100;
}
int main()
{
int a=10;
int* const b=&a;
fun(&a);
fun(b);
return 0;
}
引用的使用
//内置类型
int Add_Int(const int x,const int y)
{
return x+y;//访问x,访问一次内存
}
int Add_Int_a(int& x,int& y)//传入a和b的地址(引用底层位指针)
{
return x+y;//访问x,需要解引用,所以访问两次
}
int Add_Int_b(const int& x,const int& y)
{
return x+y;//访问x,需要解引用,所以访问两次
}
/*对于内置类型直接使用Add_Int(int x,int y)这样的值传递,减少内存访问,不适合使用引用,因为不需要通过形参的改变来反作用于实参
*/
int main()
{
int a=10,b=20;
int c=0;
c=Add_Int(a,b);
cout<<c<<endl;
return 0;
}
函数是否需要通过形参的改变来反作用于实参来改变实参–>是否使用引用做形参
//自己设计的类型
struct Stud
{
int ar[10];
int num;
};
void funa(struct Stud s)//需要将s1赋值给s,调用时s也要开辟10个整型空间
{
}
void funb(struct Stud const& s)//别名,调用此函数是无需额外开辟空间,&s就是&s1,相当于指针{} //只需要传递4个字节,形参s的改变有可能改变实参s1,加上 //const修饰,只读不可修改
void func(struct Stud* s)
{
if(s==NULL) return;
//需要断言或者判空
}
int main()
{
struct Stud s1{1,2,3,4,5,6,7,8,9,10,10};
funa(s1);
funb(s1);//针对于自己设计的类型,形参最好使用引用形式,节省空间开辟并省去安全性检查
int a=10;
int& b=a;
const int& c=a;
//const int* const c=&a; 常引用,在底层表示为指向为常性
return 0;
}
在编写程序时,自己设计的类型形参能够用引用就不要用指针。