C++学习笔记---002
- C++知识语法篇
- 1、缺省参数
- 1.1、什么是缺省参数?
- 1.2、缺省参数分类
- 1.3、缺省参数的总结
- 2、函数重载
- 2.1、什么是函数重载?
- 2.2、C支持函数重载?
- 2.3、那么对于函数重载,函数名相同该如何处理?
- 2.4、那么C++是如何支持重载?
- 3、引用
- 3.1、什么是引用?
- 3.2、引用可做参数
- 3.3、引用的特性
- 3.4、引用和指针的关系
- 3.5、常引用
- 3.6、引用的作用
- 3.7、野引用
- 3.8、引用的总结
C++知识语法篇
前言:
前篇内容对于C++有一个基本认识,这篇文章开始将学习C++与C语言优化后的语法知识部分。
/知识点汇总/
1、缺省参数
1.1、什么是缺省参数?
概念:
缺省参数是用来声明或定义函数时为函数的参数指定一个缺省值。
在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
#include <iostream>
void Func(int a = 0)//缺省参数(形参)
{std::cout << a << std::endl;
}
int main()
{Func();//0,没有传参时,使用参数的默认值Func(10);//10,传参数时,使用指定的实参return 0;
}
1.2、缺省参数分类
1.全缺省参数
2.半缺省参数
另外,比如有三个参数,却只传了一个或两个参数,称为半缺省参数。否则,三个参数全是缺省参数时,称为全缺省参数。
值得注意的是,对于多参数的传参时。不能隔开/条约的传参,需从左向右的进行传参。
#include <iostream>
void Func(int a = 1, int b = 2, int c = 3)//缺省参数(形参)
{std::cout << a << std::endl;std::cout << b << std::endl;std::cout << c << std::endl << std::endl;//endl等价于'\n'
}
int main()
{Func();//1,2,3,没有传参时,使用参数的默认值Func(10,20);//10,20,传参数时,使用指定的实参Func(10, 20,30);//不能隔开/条约的传参,需从左向右//Func(, 10, 20);//Func(, , 20);//Func(10, , 20);return 0;
}
1.3、缺省参数的总结
1.缺省参数只能在声明给,而不是在定义时给。否则,编译就会报错,语法参数不匹配
2.所有带缺省的参数必须放在参数列表的右侧,即先定义所有的非缺省参数,再定义缺省参数
3.缺省参数在公共头文件包含的函数声明中指定,不要在函数的定义中指出(如果在函数的定义中指定缺省参数值,在公共头文件包含的函数声明中不能再次指定缺省参数值)
4.缺省参数并不一定是常量表达式,可以是任意表达式,甚至可以通过函数调用给出。如果缺省实参是任意表达式,则函数每次被调用时该表达式被重新求值。但是表达式必须有意义
5.C语言不支持,但C++兼容C
6.实参个数可以与形参不同,即,半缺省和全缺省参数
2、函数重载
前言:自然言语中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
2.1、什么是函数重载?
概念:
函数重载是一种特殊情况,C++允许在同一作用域中声明几个类似的同名函数,
但是注意这些同名函数的形参列表(参数个数,类型,顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
在C++中不仅函数可以重载,运算符也可以重载。
例如:运算符 << , >> 。既可以做移位运算符,也可以做输出,输入运算符。
注意:重载函数的参数个数,参数类型或参数顺序三者中必须有一个不同。
2.2、C支持函数重载?
C语言中,不允许函数名相同,所以不支持
C++中,允许函数名相同,支持重载,但有一定的要求
要求:函数名相同时,需要参数类型不同,构成函数重载。
C++函数重载要求的参数不同,一般有三种形式:
1.参数类型不同;
2.参数顺序不同(本质还是类型不同);
3.参数个数不同。
举个直观的例子:
#include <iostream>
using namespace std;
int Add(int x, int y)
{cout << "int Add(int x, int y)" << endl;return x + y;
}
double Add(double m, double n)
{cout << "double Add(double m, double n)" << endl;return m + n;
}
void Add(double a, int b)
{cout << "void Add(double a, int b)" << endl;
}
int main()
{Add(1, 2);Add(1.01, 2.02);Add(1.001, 2);return 0;
}
2.3、那么对于函数重载,函数名相同该如何处理?
对于这个问题,不妨回顾一下,底层的编译过程,方便理解。
预处理、编译、汇编、链接分析
以栈的声明和定义为例:stack.h stack.c main.c
执行过程 | 功能 | 指令 | 生成的文件 |
---|---|---|---|
预处理 | 展开头文件、宏替换、条件编译、去掉注释 | -E stack.i main.i | main.i |
编译 | 语法、词法分析,检查语法,生成汇编代码 | -S stack.s main.s | main.s |
汇编 | 把汇编翻译成二进制机器语言,生成目标文件 | -c stack.o main.o | main.o |
链接 | 匹配符号表,结合目标文件(动态/静态链接),生成可执行程序 | main.exe |
其中链接过程的关键:符号表 --> 匹配: 函数名和函数地址
另外从函数的栈帧与销毁的角度分析,汇编中call 函数名、函数名地址(就是执行函数的入口地址,第一条语句的地址),类比数组,数组名就是首元素地址,本质就是执行对应的指令
补充:
程序中当函数有定义了,才会有函数入口地址。说明只有声明时,是没有函数的地址。只有定义没有声明,即:就是有地址没有call也是不行的。
简述:声明是承诺,定义是兑现,相辅相成的关系
所以对于这个问题可知,C语言不支持重载,因为链接时,直接用函数名去找地址,有同名函数,区分不开。
2.4、那么C++是如何支持重载?
而且当函数的声明和定义分开在不同文件时,怎么分辩?不得不提出C++对于C语言进行的优化,函数名修饰规则。
C++链接过程也会生成一个符号表,符号表里除了函数名,还存着函数的修饰名,修饰名记录保存着函数的地址。
在调用函数时,编译器会通过符号表里查看修饰名来得到函数的地址实现调用;
本质就是把参数带入进行修饰,完成函数名修饰规则,实现符号表的查表,解决函数名相同的处理。
但是请注意,对于不同的编译器,底层的规范有所不同,但原理相同。
为了方便理解,就以典型的Linux中的链接举例。
比如典型的linux环境下,是以_Znntt…(可这样理解:_Z是格式+n是函数名长度+n(函数名name)+tt…(1个/多个参数的类型type))格式的函数名修饰规则
int Add(int x, int y);
double Add(double m, double n);
void Add(double a, int b);
int A(int x, int y);
int main()
{//转汇编观察Add(1, 2);//call _Z3AddiiAdd(1.01, 2.02);//call _Z3AddddAdd(1.001, 2);//call _Z3AdddiA(1, 2);//call _Z1Aiireturn 0;
}
再了解一下同样的代码,比如Vs2019环境下,是以 ?n@@YAttt@Z(理解记忆:?格式+n(函数名name)+@@YA格式+ttt…(返回值类型+参数类型)+@Z格式)
int Add(int x, int y);
double Add(double m, double n);
void Add(double a, int b);
void A(int x, int y);int main()
{Add(1, 2);//call ?Add@@YAHHH@ZAdd(1.01, 2.02);//call ? Add @@YA NNN @ZAdd(1.001, 2);//call ? Add @@YA XNH @ZA(1, 2);//call ? A @@YA XHH @Zreturn 0;
}
char --- D
int ---- H
double --- N
void --- X
.....
3、引用
3.1、什么是引用?
概念:
引用不是新定义一个变量,而是给已存在的变量起一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
所以改变引用(别名)的值,就可以改变原变量的值
格式:类型&引用变量名(C++称为对象名) = 引用实体
int a = 10;
int& b = a;
b = 20; ---> a=b=20
举例:
#include <iostream>
using namespace std;
int main()
{int a = 0;//引用,b就是a的别名int& b = a;cout << &b << endl;cout << &a << endl;b++;a++;int& c = a;int& d = c;d++;return 0;
}
3.2、引用可做参数
引用常见于作为参数使用。
#include <iostream>
using namespace std;
void Swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}
// 引用做参数
void Swap(int& a, int& b)
{int tmp = a;a = b;b = tmp;
}
int main()
{int x = 0;int y = 1;Swap(&x, &y);Swap(x, y);return 0;
}
3.3、引用的特性
1.引用必须初始化
2.引用无法改变指向,所以无法完全替换指针
#include <iostream>
using namespace std;
int main()
{int a = 0;//1.引用必须初始化//int& b;//b = c;//2.引用定义后,不能改变指向int& b = a;int c = 2;b = c;//这句是赋值,并不是改变指向//3.一个变量可以有多个引用,多个别名int& d = b;return 0;
}
3.4、引用和指针的关系
指针和引用的功能是类似的,存在重叠性的;
C++的引用,对指针使用比较复杂的场景下能更好的进行替换,让代码更间接易理解,但是不能完全替代掉指针。
引用不能完全替代指针的原因:引用定义后,不能改变指向。
比如典型的链表,指针可以指向下一个结点,上一个结点,而引用无法改变其指向
#include <iostream>
using namespace std;
struct Node
{struct Node* next;struct Node* prev;int val;
};
//一级指针
void Pushback(struct Node* phead, int x)
{phead = newnode;
}
//对比
//二级指针
void Pushback(struct Node** phead, int x)
{*phead = newnode;
}
//引用
void Pushback(struct Node*& phead, int x)
{phead = newnode;
}
int main()
{struct Node* plist = NULL;return 0;
}
#include <iostream>
using namespace std;
typedef struct Node
{struct Node* next;struct Node* prev;int val;
}Node,*PNode;//对比:二级指针和typedef+引用
void Pushback(Node** phead, int x)
{*phead = newnode;
}
void Pushback(PNode& phead, int x)
{phead = newnode;
}
int main()
{PNode plist = NULL;return 0;
}
3.5、常引用
const对引用变量的影响。
void text()
{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;
}
3.6、引用的作用
1.引用作为参数(a.输出型参数,b.作为传出参数,对象比较大时,可明显减少拷贝,提高效率)
2.引用做返回值(a.修改返回对象,b.减少拷贝,提高效率)
void Swap(int& a, int& b);
int* preorderTreversal(struct TreeNode* root, int* returnSize);
int* preorderTreversal(struct TreeNode* root, int& returnSize);
对象比较大时,可明显减少拷贝,提高效率
指针也可以,引用这种场景更据性价比
#include <iostream>
#include <time.h>
using namespace std;struct A
{ int a[100000];
};
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void main()
{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;
}
3.7、野引用
类似于局部变量,返回变量出了函数作用域其生命周期就结束了,那么就不能用引用返回变量数据。
那么什么时候使用可以用引用返回?
1.static修饰变量 --静态变量
2.全局变量
3.堆区的变量
#include <iostream>
using namespace std;
//int func()
//{
// int a = 0;
// return a;
//}
//返回a的别名,但是此时函数结束后栈区上会销毁,那么去访问a属于非法访问,形成野引用。
//非法访问导致随机值。
int& func()
{int a = 0;return a;
}
int& fun()
{int a = 0;return a;
}
int main()
{int ret = func();//开辟一个整型空间//语法概念上引用就是一个别名,没有独立空间//int& ret = func();//并且此时给a的别名起了一个别名。导致两次非法访问,野引用。cout << "ret = " << ret << endl;fun();cout << "ret = " << ret << endl;return 0;
}
语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
纯C++写法:引出:1.结构体中可直接定义函数。2.引用作返回值
#include <iostream>
#include <assert.h>
using namespace std;
//C++ 结构体 == 类
struct SeqList
{//成员变量int* a;int size;int capacity;//成员函数void Init(){a = (int*)malloc(sizeof(int) * 4);size = 0;capacity = 0;}void PushBack(int x){//判容量if (size == capacity){int newcapacity = capacity == 0 ? 4 : capacity * 2;int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);if (tmp == NULL){perror("realloc fail");return;}a = tmp;capacity = newcapacity;}a[size++] = x;capacity--;}//引用别名,结合了SLModity和SLGet//由此可见:引用别名具备双重功能int& SLGet(int pos){assert(pos >= 0);assert(pos < size);return a[pos];}//提前了解C++更优的写法;int& operator[](int pos){assert(pos >= 0);assert(pos < size);return a[pos];}
};
int main()
{SeqList s;s.Init();s.PushBack(1);s.PushBack(2);s.PushBack(3);s.PushBack(4);for (int i = 0; i < s.size; i++){//cout << "SLGet=" << s.SLGet(i) << endl;cout << "SLGet=" << s[i] << endl;//等价原型://cout << "SLGet=" << s.operator[](i) << endl;}cout << endl;for (int i = 0; i < s.size; i++){//if (s.SLGet(i) % 2 == 0)//{// s.SLGet(i) *= 2;//}if (s[i] % 2 == 0){s[i] *= 2;}}for (int i = 0; i < s.size; i++){//cout << "SLGet=" << s.SLGet(i) << endl;cout << "SLGet=" << s[i] << endl;//等价原型://cout << "SLGet=" << s.operator[](i) << endl;}cout << endl;free(s.a);return 0;
}
3.8、引用的总结
(1).引用和指针的区别:
1.在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
2.在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
(2).引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针可以初始化。可以不初始化。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;即:引用不能改变指向,而指针可以灵活更变指向。
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全,没有NULL引用,但有NULL指针,容易出现野指针,但不容易出现野引用,但存在野引用。
(3)引用的作用:
1.引用作为参数(a.输出型参数,b.作为传出参数,对象比较大时,可明显减少拷贝,提高效率)
2.引用做返回值(a.修改返回对象,b.减少拷贝,提高效率)