函数设计_高质量C++/C编程指南

第六章 函数设计

函数接口的两个要素是参数和返回值。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 使用断言

assert参考

程序一般分为 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;
}

引用的特点

  1. 定义引用必须初始化
  2. 没有空引用
  3. 没有引用的引用(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;
}

引用和指针的区别(重点)

引用是指针的语法糖

语法规则上的区别
  1. 从语法规则上将,指针变量存储某个实例(变量或对象)的地址;引用则是某个实例的别名。
  2. 程序为指针变量分配内存区域(32位4字节x86,64位8字节x64);而不为引用分配内存区域。
  3. 在使用指针和引用所指向或别名的实例时,解引用是指针使用时要在前加“*”;引用可以直接使用。
  4. 指针变量的值可以发生改变,存储不同实例的地址(修改所指向的地址,eg:指针++or指针–);引用在定义时就别初始化,之后无法改变(不能再是其他实例的引用)。
  5. 指针变量可以为空(空指针NULL);没有空引用。eg:形参的区别,形参位指针变量则需要判空或者断言,形参为引用则无需此操作。
  6. 指针变量作为形参时需要测试它的合法性(判空NULL);引用不需要判空,则更加安全。
  7. 对指针变量使用“”sizeof“得到的时指针变量的大小(32位4字节x86,64位8字节x64);对引用变量使用”sizeof“得到的大小是所引用变量的大小。
  8. 理论上指针的级数没有限制;但引用只有一级。即不存在引用的引用,但可以有指针的指针。区别:int && a;右值引用。
  9. 对指针变量的操作,会使指针指向下一个实体(变量或对象)的地址,而不知改变所指实体(变量或对象)的内容。对引用的操作直接反映到所引用的实体(变量或对象)。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=&rx;
    
    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{1234567891010};
    funa(s1);
    funb(s1);//针对于自己设计的类型,形参最好使用引用形式,节省空间开辟并省去安全性检查
    int a=10;
    int& b=a;
    const int& c=a;
    //const int* const c=&a;	常引用,在底层表示为指向为常性
    
    return 0;
}

在编写程序时,自己设计的类型形参能够用引用就不要用指针。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值