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