C++学习——缺省参数、重载函数、引用
目录
前言
一、缺省参数
1.1概念
1.2写法
1.3半缺省
1.4使用
二、重载函数
2.1.概念
2.2类型
2.3参数
2.4顺序
2.5问题
2.6原理
三、引用
1、引用是什么?
2、引用的使用方法
3、引用特性
1、引用在定义的时候必须要初始化
2、一个变量会有多个引用
3、引用一旦引用了一个实体,就不能再引用别的
4、与指针对比
5、使用场景
5.1引用作参数
5.1.1大对象作为引用参数
5.2引用作为返回值
5.2.1使用
5.2.2引用作为返回值的注意点
5.2.3引用返回用于读写修改
5.2.4权限问题
5.2.5总结
总结
前言
缺省参数的含义很好理解,重载函数是一个很有意思的用法,对比C语言有了很大的改变。
我们之前在C语言中学过指针,一个*对应着一个&,分别是解引用和取地址,那么在C++里也有一个类似的但是它只有一个&,通过这种用法这里就不是取地址的意思了,而是引用的意思,而且比较方便容易使用。
一、缺省参数
1.1概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参
1.2写法
void Func(int a = 0)//这就是缺省参数
{cout << a << endl;
}
int main()
{Func();//不传参数的时候用函数自己里面的Func(10);//传的时候用显示参数,用指定的参数return 0;
}
这里就是一个缺省参数的写法,比较简单理解。
1.3半缺省
//-----半缺省,从右往左缺省,传参从左往右
void Func(int a , int b = 15, int c = 20)
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl;
}
int main()
{//Func();Func(1); //1 15 20Func(1,2); //1 2 20可以从左到右传依次传参Func(1,2,3); //1 2 3return 0;
}
半缺省的实现就是从左往右走,没传入参数的就按照函数内的参数输出。
1.4使用
这里是一个栈。
struct Stack
{int* a;int top;int capacity;
};void StackInit(struct Stack* pst,int defaultCapacity=4)
{pst->a = (int*)malloc(sizeof(int) * defaultCapacity);if (pst->a){perror("malloc fail");return;}pst->top = 0;pst->capacity = 4;
}int main()
{struct Stack st;StackInit(&st,100);StackInit(&st);return 0;
}
如果没有defaultCapacity,那就需要不断扩容,效率低下,但我们不知道要申请多少内存,所以先给4个,不够用再扩容,默认情况下开少一点,如果想控制就显示的传入参数。
这里注意:
声明和定义不能同时给缺省,因为这时候就不知道按照谁的来了
二、重载函数
2.1.概念
我们知道C语言不允许同名函数的出现,c++则可以,自动匹配类型,构成重载需要没有歧义。
函数重载是函数中的一种特殊的情况,C++允许在同一作用域中声明几个功能类似的同名函数。这些同名函数形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题,返回值没有要求
2.2类型
//---类型
int ADD(int left, int right)
{cout << "int ADD(int left,int right)" << endl;return left + right;
}
double ADD(double left, double right)
{cout << "double ADD(double left,double right)" << endl;return left + right;
}
int main()
{cout << ADD(1, 2) << endl;cout << ADD(1.1, 2.2) << endl;return 0;
}
这里返回的类型不同,传入形参的类型也不同,所以可以实现函数重载,不会报错。
2.3参数
//----参数
void Func()
{cout << "Func()" << endl;
}
void Func(int a)
{cout << "Func(int a)" << endl;
}
一个有参数,一个没有参数,所以可以用函数重载,不会报错。
2.4顺序
//----顺序
//也可以认为是类型不同
void f(int a, char b)
{cout << "f(int a, int b)" << endl;
}
void f(char b, int a)
{cout << "f(int a, int b)" << endl;
}
这里是顺序不同,当然也可以认为是类型不同,因为两个形参的类型发生了变化,所以可以用函数重载,不会报错。
2.5问题
当无参调用的时候就会发生歧义
//----问题:无参调用存在歧义
void f()
{cout << "f()" << endl;
}
void f(int a=0)
{cout << "f(int a=0)" << endl;
}
int main()
{f();//重载调用不明确return 0;
}
这里就会出现一个报错,说是重载调用不明确,也就是不知道用哪一个函数了。所以要注意一下。
2.6原理
实际上,函数重载的实现是和它编译器汇编命名规则有关,C语言为什么不能函数重名,因为在C语言中,汇编函数的命名就是函数本身的名字,没有发生改变,所以肯定会发生冲突。而在C++中,汇编函数的命名是重新用一种函数命名的方法,它结合了形参,使两个函数名发生了不同,所以调用的时候不会发生冲突从而实现了函数重载。
三、引用
1、引用是什么?
引用不是新定义的一个变量,而是给已经存在的变量去一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共同用一块空间。
在C++中,引用是一种别名,用于引用已存在的变量或对象。它允许我们通过一个变量名来访问另一个变量的值,而无需使用指针。
引用通常用于函数参数、函数返回类型、以及作为对象的别名。引用在声明时必须进行初始化,一旦初始化后,它将一直引用同一个对象,无法改变引用的目标。引用在使用上类似于原始变量,但是对引用的操作实际上是对原始变量的操作。引用的主要优点是可以避免拷贝大型对象,提高程序的效率。
什么是别名呢,比如你是李白,你又字太白,所以我可以管你叫李白,也可以管你叫“李太白”,但是这两个名字是否是一个人呢?当然如此,就是一个对象,有不同的叫法。
2、引用的使用方法
我们直接给出代码:
int main()
{int a = 0;int& b = a;int& c = b;a = 10;cout << a << " " << b << " " << c << " " << endl;cout << &a << " " << &b << " " << &c << " " << endl;return 0;
}
这里给出了一个变量a,引用b,也就是a的别名可以是b,引用c,b的别名是c,而b又是a的别名,所以c是a的别名,b和c实际上就是a,这里a赋给10,那么输出来的三个变量也都是10。我们不妨可以运行一下:
我们可以看见都是a的地址,他们也都是10,这就是引用。
3、引用特性
引用这里也有几个要注意的特性:
1、引用在定义的时候必须要初始化
int main()
{int a = 10;int& b;return 0;
}
这里要是不初始化的话就会报错。
2、一个变量会有多个引用
比如我们之前的代码:
int a = 0;int& b = a;int& c = b;
这里a变量就有两个引用,分别是b和c。
3、引用一旦引用了一个实体,就不能再引用别的
int a = 10;
int& b = a;
int c = 5;
int& b = c;
cout << a << " " << b << " " << c << " " << endl;
return 0;
这里我们b先引用了a,然后又定义了一个c的变量,再把b引用c,这时候如果我们运行程序的话,就会发现报错。
4、与指针对比
我们以前可能写过两个数交换的函数,那时候用的是指针交换:
void swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}
int main()
{int a = 10;int b = 30;cout << "before::" << a << " " << b << endl;swap(&a, &b);cout << "after::" << a << " " << b << endl;return 0;
}
我们通过两个数的指针传入地址实现传址交换通过交换:
这里我们就可以用C++来实现,因为别名都是一个对象的,所以如果引用传入的参数,那么自函数里的参数就是主函数传入参数的别名,都是同一个变量,变相的实现了传址:
void swap(int& a, int& b)
{int tmp = a;a = b;b = tmp;
}
int main()
{int a = 10;int b = 30;cout << "before::" << a << " " << b << endl;swap(a, b);cout << "after::" << a << " " << b << endl;return 0;
}
这里运行之后还是符合的:
如果这里是传的指针呢?
void swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}
int main()
{int x = 10;int y = 30;int* a = &x;int* b = &y;cout << "before::" << *a << " " << *b << endl;swap(*a, *b);cout << "after::" << *a << " " << *b << endl;return 0;
}
这里就是参数传入的是指针,我们依旧可以用引用来实现:
把指针引用可以吗,当然可以了,请看下面的代码:
void swap(int*& a, int*& b)
{int tmp = *a;*a = *b;*b = tmp;
}
int main()
{int x = 10;int y = 30;int* a = &x;int* b = &y;cout << "before::" << *a << " " << *b << endl;swap(a, b);cout << "after::" << *a << " " << *b << endl;return 0;
}
这里就直接把指针引用了,结果运行后还是之前的:
指针和引用的不同点:
1、引用概念上定义一个变量的别名,指针存储了一个变量地址。
2、引用在定义时必须初始化,指针没有要求
3、引用在初始化时引用了一个实体后,就不能再引用其它实体,而指针可以在任何时候指向任何一个同类型的实体
4、没有NULL引用,但有NULL指针
5、在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台4个字节)
6、引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7、有多级指针,但是没有多级引用
8、访问实体方式不同,指针需要显示解引用,引用编译器自己处理
9、引用比指针使用起来相对更安全
5、使用场景
5.1引用作参数
引用作为参数就是为了提升效率,针对两种情况最适合用引用来作为参数,第一个是大对象,第二个是深度拷贝对象。深度拷贝是指在拷贝对象时,不仅要复制对象的成员变量的值,还需要为新对象分配独立的内存空间,并将原对象的值复制到新对象的内存空间中。这里先接触大对象,后期再接触深度拷贝。
5.1.1大对象作为引用参数
大对象是什么意思,就是通过sizeof后,比较大的对象。
我们可以用下面的代码进行测试:
struct A { int a[10000]; };
void test1(A a) {}
void test2(A& a){}
void testref()
{A a;//以值作为函数参数size_t begin1 = clock();for (size_t i = 0; i < 10000; ++i)test1(a);size_t end1 = clock();//以引用作为函数参数size_t begin2 = clock();for (size_t i = 0; i < 10000; ++i)test2(a);size_t end2 = clock();//分别计算两个函数运行结束后的时间cout << "test1_time::" << end1 - begin1 << endl;cout << "test2_time::" << end2 - begin2 << endl;
}int main()
{testref();return 0;
}
这里定义了一个结构体,里面有一个四万字节的整形数组,定义了两个函数,一个是单独的传值,一个用了引用,这里我们可以通过time库函数计时来看到明显的区别:
可以看到时间效率的快慢,对象越大,代价越大。
5.2引用作为返回值
5.2.1使用
我们先看这一块代码:
int Func()
{static int n = 0;n++;return n;
}
int main()
{int a=Func();return 0;
}
Func函数内变量n是在静态区的,当函数调用后n++,但这里不是直接n赋给了主函数里的a,而是通过一个中间的一个寄存器或者其他临时变量接收,然后再传给a,函数栈帧也销毁。
这样我们就可以知道,中间的步骤也是需要时间的,如何才能绕过中间的寄存器或者临时变量呢?这里就可以用引用作为返回值。
int& Func()
{static int n = 0;n++;return n;
}
int main()
{int a=Func();return 0;
}
函数返回的是n,这里引用了n,赋给了a,所以返回值n就是a的一个别名,直接跳过了中间的一系列操作。
如果不生成临时变量好处就是减少了拷贝,提高了效率,在面对大对象的时候影响会很大
我们同样可以用time库来测试一下效率:
struct A { int a[10000]; };
A a;
A test1() { return a;}//值返回
A& test2() { return a;}//引用返回
void testref()
{//以值作为函数返回值类型size_t begin1 = clock();for (size_t i = 0; i < 10000; ++i)test1();size_t end1 = clock();//以引用作为函数返回值类型size_t begin2 = clock();for (size_t i = 0; i < 10000; ++i)test2();size_t end2 = clock();//分别计算两个函数运行结束后的时间cout << "test1_time::" << end1 - begin1 << endl;cout << "test2_time::" << end2 - begin2 << endl;
}
这里就是通过值返回和引用返回,运行下来可以看到明显的差距:
5.2.2引用作为返回值的注意点
我们思考一个问题:函数栈帧的创建和销毁应该是在一个作用域内的,如果出了这个作用域,就被系统回收销毁了,那么我们在传值引用的时候会不会出现问题?
答案是有可能会的,而且很不安全,所以一定要谨慎使用,这完全取决于栈帧销不销毁,取决于系统,栈帧不销毁我们就可以用,但如果销毁了我们就只能用传值。
我们在用返回值引用的时候,有以下几种情况可以使用:
1、static静态区变量
2、全局变量
3、malloc内存
5.2.3引用返回用于读写修改
我们定义了一个顺序表,这里用的静态顺序表方便演示:
struct SeqList
{int a[100];size_t size;
};
如果提供一个函数可以对它修改而且还可以获取当前修改的地方,怎么写?
int& SLAt(SeqList& ps, int pos)
{assert(pos < 100 && pos >= 0);return ps.a[pos];
}
通过引用返回就可以实现,这里引用的是当前要修改位置的变量,通过以下操作就可以实现:
int main()
{SeqList s;SLAt(s, 0) = 1;cout << SLAt(s, 0) << endl;SLAt(s, 0) += 5;
}
这里就是表的第0个位置修改成1,第0个位置加上5。
5.2.4权限问题
引用返回也有权限问题,这里先说结论:
引用的过程中,权限不能放大,可以缩小或者平移
//引用的过程中,权限不能放大const int a = 0;int& b = a;
这里原来a是常变量,引用后是一个没有限制的整形,所以权限发生了变大,是不能的。
// //引用过程中,权限可以平移或者缩小int x = 0;int& y = x;
还有一种就是修改的 方法:
int& y = x;const int& z = x;//缩小的是z作为别名的权限,x作为别名z的时候不能修改,当为x的时候可以修改,这里可以修改x,y来改变z,但是不能通过z来改变
这里z因为是常量,所以不能进行修改,但是可以通过x来修改,可以通过x和y来修改z的值。
下面这段代码可不可以运行,可以,因为会发生整形提升:
int main()
{double dd = 1.11;int ii = dd;
}
当我们用引用呢?
int main()
{double dd=1.11;int& rii = dd;//const int& rii = dd;
}
答案是不能。也许你会想以你为是因为不同类型的原因,当然这也是一部分原因,但还有另一部分原因,就是在这里不同类型赋值的时候,中间会出现一个临时变量,而这个临时变量是有常性的,所以不能。如果给上一个const就可以实现。
5.2.5总结
1、基本任何场景都可以用引用传参
2、谨慎用引用做返回值,出了函数作用域,对象不在了,就不能用引用返回,还在就可以用引用返回.
总结
今天主要对缺省参数进行了了解,重载函数进行了了解,重点对引用进行了学习。