🌈初识C++
一段C++版的hello world
C++是在C的基础之上,容纳进去了面向对象编程思想,增加了许多有用的库,也弥补了许多C语言的不足。
🌈命名空间
来解决C语言明明冲突的问题。
☀️C语言的命名冲突问题
当在C语言中定义一个名叫rand的变量,可能程序会出错,因为在stdlib.h库中,rand是一个函数,命名冲突了:
☀️C++命名空间与域作用限定符
🎈1.对命名空间内变量的访问
定义一个名叫bit的命名空间,该空间内部有一个叫rand的变量,当我想使用该变量时,就用"空间名+域作用限定符+变量名"的方式",即"bit :: rand",当rand前面什么都没有时,默认从全局变量中找rand,即rand函数。
🎈2.命名空间内可以放变量、函数、结构体等,都用 : : 符号来限定。
🎈3.命名空间的内部嵌套
如果同一个命名空间内部两名称冲突,那就在该空间内继续嵌套空间,隔开冲突的名称。
假设大空间bit内部包含了两个小空间bit1和bit2,此时对小空间的访问方式是"大空间+小空间+域作用限定符+变量名",对bit1空间内访问是"bit : : bit1: : rand"。
🎈4.合并命名空间
同一个文件的多个位置出现同一个命名空间,或多个文件中出现同一个命名空间,编译器会将他们合并。
例如:Stack.h文件的bit空间内声明了两个函数,Stack.cpp文件中的bit空间内是这两个函数的定义,Test.cpp文件中用域作用限定符对函数进行访问。由于编译器的合并命名空间功能,从而也可以实现分文件操作。
🎈5.默认指定展开
(1)整体展开
即默认指定全部命名空间。
如果要用bit空间内的变量、函数等,还要每次都要指定bit空间,即写"bit : : (...)"太麻烦了,而如果设置默认访问bit空间,即默认使用的每一个函数或者变量都是bit空间中的,就不需要麻烦的写"bit : : (...)"了。
指定命名空间语法:using namespace (空间名)
例:展开指定C++标准库定义的命名空间std:
注:工程项目不要展开std,容易冲突,日常练习可以。最安全的方式是指定展开我们自己创建的命名空间,需要哪个展开哪个。
(2)部分展开
即默认指定命名空间内部分的变量、函数等。全部展开风险太大,部分展开可以规避风险。
cout是C++的输出流,cin是输入流,<<是流插入运算符,>>是流提取元运算符,都储存在空间std中。当我们不全部展开,只展开部分常用的,就可以在规避风险的同时实现便利。
例:部分展开前后对比:
当std不是默认指定空间时,用cout输出变量a和b:
太麻烦,指定cout为默认访问:
🌈C++输入&输出
cout是输出(流输出),cin是输入(流提取),cout、cin都是定义在头文件iostream中的。
☀️使用cout和cin打印与输入数据
将std库中的cout设为默认后,直接使用:
☀️C++可以兼容C
目前还不知道怎样用C++控制数据精度,可以借助C语言打印精度更高的数据:
🌈缺省参数
☀️缺省参数概念:
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。调用时可传参也可不传参,不传参时,参数值就是指定的缺省值。缺省值必须是常量或者全局变量。
☀️缺省参数的声明与定义
当函数既有声明又有定义时,缺省参数不可以在声明和定义中都出现,防止两个参数值不一样。也不能只在定义中出现声明中不出现,这样包含头文件时会不知道声明中的参数是几。只能单独在声明中出现。
.h文件中的声明里需要出现缺省值:
.cpp文件中的定义里不可出现缺省值:
☀️缺省参数的分类
🎈1.全缺省参数
所有参数都是缺省参数,声明时需要指定默认值:
c
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
🎈2.半缺省参数
只有后半部分参数是缺省参数:
c
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
注:半缺省参数必须从右往左依次来给出,不能从左往右给,也不能间隔着给。(如果从前往后设置缺省参数或者间隔着设置,那么编译器会不知道哪个对应的是缺省参数)
☀️缺省参数应用
比如在栈初始化申请空间时,不论知不知道要多大的空间,都可以用同一个函数。具体方法是使用缺省参数设置缺省值,该值表示默认开辟的空间大小,知道需要多少空间就传参,不知道就不传参使用缺省值。
🌈函数重载
☀️函数重载概念:
C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
注:返回值可同可不同,具体取决于返回的数据的类型。
类型不同:
个数不同:
☀️注:函数重载要避免歧义
Add有两个重载函数,第一个的两个参数都是int类型的,第二个的两个参数都是double类型的:
此时,如果一个函数调用为Add(1,2),则会匹配到Add(int left,int right);如果一个函数调用为Add(1.1,2.2),则会匹配到Add(double left,double right)。但是当函数调用为Add(1,2.2)时,既可以匹配到Add(int left,int right)(此时会将第二个参数转换成int类型),也可以匹配到Add(double left,double right)(此时将第一个参数转换成double类型),有了歧义,这时编译器不知道匹配哪一个函数,会报错。
再比如,函数f有两个重载函数,第一个无参数,第二个有一个缺省参数:
c
void f()
{
cout << "f()" << endl;
}
void f(int a=1)
{
cout << "f(int a)" << endl;
}
此时,如果不传参调用f函数,可以匹配到第一个无参数的f函数,也可以匹配到第二个不传值情况下的缺省函数,产生歧义,编译器报错。不传参时调用存在二义性:
🌈引用(🌟🌟🌟)
☀️一、引用概念:
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。那么电脑中的同一块空间就有了三个名字:李逵、铁牛、黑旋风,通过这三个名字都可以找到这块空间。
同一个变量可以有多个别名,即多次引用:
☀️二、C的指针&C++的引用
C的传址调用:调用时需要传地址,函数内部接收到后需要解引用
C++的引用:调用时直接用变量名,函数内部以取别名的方式接收变量
第一个Swap是C语言版本的传地址,第二个Swap是C++版本的引用。第二个Swap函数表示,left是a的别名,right是b的别名,left改变则a改变,right改变则b改变。
🎈1.引用是指针的便捷方式
(1)分析C语言传址调用的不便:
不便一:传值调用只改变形参不改变实参,无法达到对数据的修改。
不便二:传几级指针就需要几级的解引用,如果指针的级数和解引用的级数不匹配,则会出错,而多级指针很容易出现不匹配的错误。
以顺序表传头节点指针为例:
节点的内容存放在结构体SLTNode中 -> 通过结构体指针访问节点 -> 头节点指针是一个一级结构体指针 -> 传参时为了能进入指针内部,需要传一级指针的地址,即二级指针 -> 接收时用二级指针接收
错误的调用方式:
phead是plist的拷贝,形参的改变不影响实参,phead=newnode无法让plist也等于newnode。
正确调用方法:
只有通过取plist的地址,再对该地址解引用,才能让phead的值改变。
(2)用C++的引用解决不便
🌟插入新知识:typedef结构体指针名
当对结构体重命名时,如果名称前有*符号,则该名称代表指向该结构体变量的指针名称。比如:
PSLTNode是结构体指针名,PSLTNode = &SLTNode。
--- --- --- --- --- --- --- --- ---👻分割线👻 --- --- --- --- --- --- --- --- ---
使用引用方式,传参时传结构体指针名,接收参数时用一个别名接收,就可大大简化传地址的不便:
节点名称:STLNode
节点指针:STLNode* 。STLNode*=PSTLNode
(直接在节点的基础上对结点指针重命名)
在之前的基础上有两方面的改动:
1.将所有STLNode*替换成PSTLNode
2.用引用的方式将结点指针类型的变量起别名为phead,即用&PSTLNode phead的方式接收传来的参数。
🎈2.指针不可被引用替代的方面
(1)引用必须初始化,指针可以是NULL
引用必须初始化,就是说明起的是谁的别名。不能人还没有呢就先来个别名。
(2)引用不可以改变指向,指针可以
值变,地址不变,说明没有改变c的指向,只改变了c里面的值。只能给A增加或更换别名,不能把A的别名拿着说这是B的别名。
由于C++无法改变指向即地址,因此数据结构的某些部位如链表还是要用指针。比如在链表中插入一个节点,此时需要改变指针的指向,无法用引用的方式完成。
🎈3.总结引用和指针的异同点
(1)相同点
引用和指针的底层都开辟空间了。
(2)不同点
1.引用概念上定义一个变量的别名,指针存储一个变量地址。
-
引用在定义时必须初始化,指针没有要求。
-
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。即引用不可以改变指向,指针可以。
-
没有NULL引用,但有NULL指针。
-
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
-
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
-
有多级指针,但是没有多级引用。
-
访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
-
引用比指针使用起来相对更安全。
☀️三、引用作参数、引用作返回值
🎈1.做参数,方便好理解
🎈2.做返回值
(1)引用返回的风险
引用返回的本质是野指针:
返回值原本在函数的栈帧中有一席之地,但函数运行完被销毁了,返回值所在的空间也被销毁了(被销毁不是说空间消失了或不能用了,意思是这片空间不受管辖,可以随意被分配给其他地方装载其他值)。引用返回,返回的是变量n的别名,通过该返回值的别名找到这块不受管辖的空间,就如同野指针。
对比传值返回:函数调用结束,返回的变量也跟着销毁,所以实际返回的不是变量本身,而是变量的拷贝。
说明在vs下,销毁栈帧不会换成随机值,在被其他值覆盖前保持之前函数在此处放的值。其他编译器下不一定。
(2)引用返回的正确用法
在被返回的值前加static,被static修饰的局部变量只会被初始化一次。
①此时的ret已经和Add(1,2)的返回值绑定:
c
#include <iostream>
using std::cout;
using std::endl;
int& Add(int a, int b) {
static int c = a + b;
return c;
}
int main() {
int& ret = Add(1, 2);
cout << "Add(1,2) is " << ret << endl;
Add(3, 4);
cout << "Add(3,4) is " << ret << endl;
}
②在(1)的 基础上实现返回值的变动,此时ret和c这块空间内放的任何数据都绑定:
c
#include <iostream>
using std::cout;
using std::endl;
int& Add(int a, int b) {
static int c ;
c = a + b;
return c;
}
int main() {
int& ret = Add(1, 2);
cout << "Add(1,2) is " << ret << endl;
Add(3, 4);
cout << "Add(3,4) is " << ret << endl;
}
☀️四、传值、传引用效率比较
🎈 1.传值调用&传引用调用
测试对比传值调用和传引用调用的时间差异:
c
#include <iostream>
using std::cout;
using std::endl;
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(struct A a) {}
void TestFunc2(struct A& a) {}
//也可以不加struct写成:void TestFunc1(A a) {}
// void TestFunc2(A& a) {}
void TestRefAndValue()
{
struct 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;
}
int main() {
TestRefAndValue();
return 0;
}
结论:传引用调用效率高。
🎈2.传值返回&传引用返回
c
#include <iostream>
using std::cout;
using std::endl;
#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;
}
int main() {
TestReturnByRefOrValue();
return 0;
}
结论:传引用返回效率高得多。
🎈3.引用返回可做左值,方便修改返回对象
传值返回时,返回的是一个拷贝的值,这个值只能放在等号右边,即必须要用一个变量接收这个返回值;引用返回,返回的是下标为pos的那个位置的别名,可以充当等好的左边,可以被改变。因此对这个位置赋值或修改是很方便的(比指针方便,指针还要解引用呢)。
☀️五、常引用
🎈1.常引用概念:
简单来说就是给变量或者常量取带有常属性的别名,具有了常属性就不可被修改了。
一个变量前加上const后就具有了常属性。从变量到常量属于权限的缩小;从常量到常量属于权限的平移;从常量到变量属于权限的放大。
🌟复习一下宏:
1.概念:宏就是一种单纯的替换,没有类型,可以替换函数表达式、变量常量等;宏后面不可加分号;宏会被替换成表达式。
2.注:如果宏用来替换一个表达式,则对每一项运算对象都加括号,防止计算对象本身就是表达式。例如:
如果宏定义的ADD是:ADD(x,y) (x+y),则容易出现运算符优先级的错误。
2.两种常引用操作
(1)对无常属性变量常引用
c
int a = 10;
const int& ra = a;
ra是a的别名,同时ra具有常属性,从a到ra属于权限的缩小。
(2)对有常属性变量常引用
c
const int a = 10;
const int& ra = a;
ra是a的别名,a本身就具有常属性,ra也有常属性,从a到ra属于权限的平移。
❌易犯错误:给具有常属性的变量起没有常属性的别名,属于放大权限:
c
const int a = 10;
int& ra = a;
(3)对常量常引用
c
const int& b = 10;
10本身是一个常量,b是10的别名,因此也必须有常属性,从10到b属于权限的平移。
❌易犯错误:给常量起没有常属性的别名,属于放大权限:
c
int& b = 10;
(4)别名的数据类型不同
c
double d = 12.34;
const int& rd = d;
整型可以隐式转换成double类型,内部过程是整型的i先存进一个临时变量中转换成double,再存储到double类型的变量j中,由于临时变量具有常属性,因此给double类型的引用加上const后,就可以存进整型的i了。简单来说,类型不一样,进行转换时,加个const就可以了。
❌易犯错误:
c
double d = 12.34;
int& rd = d;
🌈内联函数
☀️内联函数概念:
以inline修饰的函数叫做内联函数,在编译期间编译器会用函数体替换函数的调用。相当于C语言的宏。
注:C++中替代宏的技术:
1.常量定义,换用const enum
- 短小函数定义,换用内联函数
☀️内联函数特性
1.inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。
缺陷:可能会使目标文件变大。
优势:没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
2.inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
☀️使用内联函数注意事项
内联只适合小函数(10行以内),当大函数用内联时(假设大函数100行,一共调用量10000次),则展开指令合计100*10000行,不展开指令合计100+10000行。内联函数会影响可执行程序大小,影响更新的大小和时间,无条件地使用内联函数会让程序很庞大。
🌈 auto关键字
变量前加上auto关键字,可以识别出这个是什么类型的变量,从而替代了原先的准确变量类型。
typeid是打印类型函数,typeid(变量名).name()。
注:如果想让auto推导出引用的话,必须写成auto&,此时推导出的类型是int
☀️auto真正的意义:
🎈1.代替长类型,如
变量类型是vector< s t r i n g string string>: :iterator,太长了,用auto简化:
注意:
1.用auto替代类型名的变量必须要在右边给值初始化 :
2.auto不可以作为函数返回值, 因为如果不调用该函数,或者无返回值,此时编译器就不知道推导什么了:
3.auto不可以声明数组:
🎈2.用于遍历数组
(1)C语言通过下标或指针来遍历数组:
(2)C++基于数组遍历:
依次取数组中的数据赋值给e,自动判断结束,自动++往后走。也可以用int,但auto很方便,适用于各个类型的数组。
注:e是对数组数据的拷贝,对e的修改不影响实际数组中的数据,即下面的写法不起作用:
如果想要实现堆数组中数据的改变,则需要用引用的方式让e数组数据共用同一块空间:
注:错误写法:
此时不可以这样写,因为传参传进来的是指针不是数组(以array[ ]接收到的参数是指针,相当于(int* array)),即array在这个函数中是一个指针名而不是数组名,for循环写法只能基于数组。
🌈 NULL和nullptr
☀️NULL:
NULL本质上是一个宏,有些极端情况下被替换成数字0,从而引发麻烦,如果硬要将NULL按照指针方式来使用,必须对其进行强转(void ∗ * ∗)。
☀️nullptr:
C++中空指针换成nullptr,能用NULL的情况下都能用nullptr,而且还不会被替换成0。nullptr等价于(void ∗ * ∗)NULL。
证明:
当传参为NULL时:
(注:函数用于接受参数的变量只有类型无变量名,但不会报错,因为程序不用传进来的值,只要传一个整型数据就行)
证明NULL被当成数字而不是指针。
当传参为nullptr时:
证明nullptr就是指针类型。