各位CSDN的uu们你们好呀,今天是小雅兰的C++专栏呀,初识C++下篇来啦,下面,让我们进入C++的世界吧!!!
缺省参数
缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
缺省参数相当于现实生活中的备胎!!!
cpp#include<iostream> using namespace std; void Func(int a = 0) { cout << a << endl; } int main() { Func(); // 没有传参时,使用参数的默认值 Func(10);// 传参时,使用指定的实参 return 0; }
缺省参数分类
全缺省参数
cppvoid Func(int a = 10, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; }
函数调用的话,可以不传参数,也可以传一个参数,也可以传两个参数,也可以传三个参数。
但是C++不支持这样一种玩法,就是你只想给第二个参数传参,其余的不给;或者你只想给第三个参数传参,前面两个不想给。
cppint main() { Func(); Func(1); Func(1, 2); Func(1, 2, 3); return 0; }
半缺省参数(只缺一部分参数)
cppvoid Func(int a, int b = 10, int c = 20) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl << endl; }
必须传一个参数!!!
传一个也可以,传两个也可以,传三个也可以。
- 半缺省参数必须从右往左依次来给出,不能间隔着给
cppint main() { Func(1); Func(1, 2); Func(1, 2, 3); return 0; }
之前C语言写栈的时候,初始化栈和入栈是这样写的,得扩容:
cpp// 初始化栈 void StackInit(Stack* pst) { assert(pst); pst->a = NULL; pst->top = 0; pst->capacity = 0; } // 入栈 void StackPush(Stack* pst, STDataType x) { assert(pst); //扩容 if (pst->top == pst->capacity) { int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2; STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail"); return; } pst->a = tmp; pst->capacity = newcapacity; } pst->a[pst->top] = x; pst->top++; }
现在学习了C++的缺省参数后,就可以这样:初始化栈的时候,可以给一个缺省参数,先开一部分空间,可以解决后续扩容的问题。(因为扩容本就是不稳定的,且消耗较大)
cppnamespace zyl { typedef struct Stack { int* a; int top; int capacity; }Stack; // 不允许声明和定义同时给缺省参数 // 声明给,定义不给 void StackInit(Stack* ps, int N = 4); void StackPush(Stack* ps, int x); }
cppvoid zyl::StackInit(Stack* pst, int N) { pst->a = (int*)malloc(sizeof(int) * N); pst->top = 0; pst->capacity = 0; } void zyl::StackPush(Stack* pst, int x) { // ... }
cppint main() { zyl::ST st1; StackInit(&st1, 10); for (size_t i = 0; i < 10; i++) { StackPush(&st1, i); } zyl::ST st2; StackInit(&st2, 100); for (size_t i = 0; i < 100; i++) { StackPush(&st2, i); } // 不知道可能会插入多少个 zyl::ST st3; StackInit(&st3); return 0; }
- 缺省参数不能在函数声明和定义中同时出现(声明给,定义不给)
cpp//a.h void Func(int a = 10); // a.cpp void Func(int a = 20) {} // 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该 用那个缺省值。
- 缺省值必须是常量或者全局变量
- C语言不支持(编译器不支持)
函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是"谁也赢不了!",后者是"谁也赢不了!"
函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
C语言不允许有同名函数!!!
cpp// 1、参数类型不同 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; }
这个功能非常的好用!!!
在C语言阶段,经常写Swap函数:
cppvoid Swap1(int* p1, int* p2) { //... } void Swap2(double* p1, double* p2) { //... }
C语言里面就只能这样写。
但是在C++里面,允许同名函数,只要求构成函数重载就可以了。
cppvoid Swap(int* p1, int* p2) { //... } void Swap(double* p1, double* p2) { //... }
cppint main() { int i = 1, j = 2; double k = 1.1, l = 2.2; Swap(&i, &j); Swap(&k, &l); return 0; }
自动匹配!!!
cpp// 2、参数个数不同 void f() { cout << "f()" << endl; } void f(int a) { cout << "f(int a)" << endl; } // 3、参数类型顺序不同 void f(int a, char b) { cout << "f(int a,char b)" << endl; } void f(char b, int a) { cout << "f(char b, int a)" << endl; } int main() { f(); f(10); f(10, 'a'); f('a', 10); return 0; }
注意:返回值不同不构成函数重载!!!
但是:
namespace zyl1
{
void func(int x)
{}
}
namespace zyl2
{
void func(double x)
{}
}不构成函数重载!!!因为在不同的域里面。
如果把zyl2改为zyl1,就构成函数重载了。
void func(int a)
{
cout << "void func(int a)" << endl;
}
void func(int a, int b = 1)
{
cout << "void func(int a,int b)" << endl;
}上面的两个函数确确实实构成函数重载!!!
但是,是调用存在歧义,如果是在main函数里面写func(1),上面两个函数都可以调用,编译器不知道调用哪个函数。
C++支持函数重载的原理--名字修饰(name Mangling)
为什么C++支持函数重载,而C语言不支持函数重载呢?
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
cpp
void func(int i, double d)
{
cout << "void func(int i, double d)" << endl;
}
void func(double d, int i)
{
cout << "void func(double d, int i)" << endl;
}
int main()
{
func(1, 1.1);
func(1.1, 1);
return 0;
}
为什么编译器能够这么聪明呢?为什么C++可以而C语言不行呢?C++凭什么根据参数就互相调用?
- 实际项目通常是由多个头文件和多个源文件构成,而通过C语言的编译链接,我们可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么怎么办呢?
- 所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。
- 那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。
- 由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使用了g++演示了这个修饰后的名字。
- 通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
采用C语言编译器编译后结果
结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。
采用C++编译器编译后结果
结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
Windows下名字修饰规则
对比Linux会发现,windows下vs编译器对函数名字修饰规则相对复杂难懂,但道理都 是类似的,我们就不做细致的研究了。
- 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修 饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
- 如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办 法区分。
引用
引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
类型& 引用变量名(对象名) = 引用实体;
cpp
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
//给a取了一个别名:ra
cout << &a << endl;//&表示取地址
cout << &ra << endl;
}
int main()
{
TestRef();
return 0;
}
注意:引用类型必须和引用实体是同种类型的
之前小雅兰写的二叉树的前序遍历就可以用到引用,而不用指针了!
cppint TreeSize(struct TreeNode* root) { return root==NULL?0:TreeSize(root->left)+TreeSize(root->right)+1; } //递归里面传数组下标要注意!!! //每个栈帧里面都有一个i void preorder(struct TreeNode* root,int* a,int* pi) { if(root==NULL) { return; } a[(*pi)++]=root->val; preorder(root->left,a,pi); preorder(root->right,a,pi); } int* preorderTraversal(struct TreeNode* root, int* returnSize){ //root是输入型参数,returnSize是返回型参数 *returnSize=TreeSize(root); int* a=(int*)malloc(*returnSize*sizeof(int)); int i=0; preorder(root,a,&i); return a; }
改为引用:
cppint TreeSize(struct TreeNode* root) { if (root == NULL) return 0; else return TreeSize(root->left) + TreeSize(root->right) + 1; } void _preorderTraversal(struct TreeNode* root, int* a, int& ri) { if (root == NULL) return; printf("[%d] %d ", ri, root->val); a[ri] = root->val; ++ri; _preorderTraversal(root->left, a, ri); _preorderTraversal(root->right, a, ri); } int* preorderTraversal(struct TreeNode* root, int& returnSize) { int size = TreeSize(root); int* a = (int*)malloc(sizeof(int) * size); int i = 0; _preorderTraversal(root, a, i); returnSize = size; return a; }
ri就是i的别名!!!
Swap函数也有一种新玩法:
void swap(int& x1, int& x2)
{
int tmp = x1;
x1 = x2;
x2 = tmp;
}
还有之前的单链表,也有新玩法:
typedef struct ListNode{
int val;
struct ListNode* next;
}ListNode;
//C语言二级指针的玩法
void PushBack(ListNode** pphead, int x)
{
ListNode* newnode;
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{}
}int main()
{
ListNode* plist = NULL;
PushBack(&plist, 1);
PushBack(&plist, 2);
PushBack(&plist, 3);return 0;
}
typedef struct ListNode {
int val;
struct ListNode* next;
}ListNode, *PListNode;//PListNode就是struct ListNode*
//CPP,引用的玩法
void PushBack(ListNode*& phead, int x)//ListNode*的别名
//void PushBack(PListNode& phead, int x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
// ...
if (phead == NULL)
{
phead = newnode;
}
else
{}
}int main()
{
ListNode* plist = NULL;
PushBack(plist, 1);
PushBack(plist, 2);
PushBack(plist, 3);return 0;
}
引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
cppvoid TestRef() { int a = 10; // int& ra;// 该条语句编译时会出错 int& ra = a; int& rra = a; //printf("%p %p %p\n", &a, &ra, &rra); cout << &a << endl; cout << &ra << endl; cout << &rra << endl; } int main() { TestRef(); return 0; }
也可以给别名取别名!!!
使用场景
做参数
cppvoid Swap(int& left, int& right) { int temp = left; left = right; right = temp; }
做返回值
这就相当于:n都销毁了,还返回n的别名。
这样的行为是非常危险的(有点像野指针)
cppint& Count() { int n = 0; n++; // ... return n; } int main() { int ret = Count(); // 这里打印的结果可能是1,也可能是随机值 cout << ret << endl; cout << ret << endl; return 0; }
这里是把n的别名拷贝给ret,如果栈帧没清打印的结果就是1,清了就是随机值。
cppint& Count() { int n = 0; n++; // ... return n; } int main() { int& ret = Count(); cout << ret << endl; cout << ret << endl; return 0; }
下面代码输出什么结果?为什么?
cppint& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(1, 2); Add(3, 4); cout << "Add(1, 2) is :"<< ret <<endl; return 0; }
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用 引用返回,如果已经还给系统了,则必须使用传值返回。
传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
cpp#include <time.h> struct A { int a[10000]; }; void TestFunc1(A a) {} void TestFunc2(A& a) {} void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; }
值和引用的作为返回值类型的性能比较
cpp#include <time.h> struct A { int a[10000]; }; A a; // 值返回 A TestFunc1() { return a; } // 引用返回 A& TestFunc2() { return a; } void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; }
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
struct SeqList
{
int a[10];
int size;
};//C的接口设计
//读取第i个位置的值
int SLAT(struct SeqList* ps, int i)
{
assert(i < ps->size);
// ...
return ps->a[i];
}
//修改第i个位置的值
void SLModify(struct SeqList* ps, int i, int x)
{
assert(i < ps->size);// ...
ps->a[i] = x;
}
//CPP接口设计
//读 or 修改第i个位置的值
int& SLAT(struct SeqList& ps, int i)
{
assert(i < ps.size);
// ...
return (ps.a[i]);
}int main()
{
struct SeqList s;
s.size = 3;
// ...
SLAT(s, 0) = 10;
SLAT(s, 1) = 20;
SLAT(s, 2) = 30;
cout << SLAT(s, 0) << endl;
cout << SLAT(s, 1) << endl;
cout << SLAT(s, 2) << endl;return 0;
}
引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
cpp
int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
return 0;
}
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
cpp
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
我们来看下引用和指针的汇编代码对比:
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
常引用
cpp
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
好啦,小雅兰今天的学习内容就到这里啦,还需继续加油学习西嘎嘎!!!