【C++(NOTES)】————(1)入门
一、 什么是C++
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
二、 C++输入&输出
- 使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空间。
数据的输出
作用:用于向显示器打印输出数据关键字:
cout
语法:
cout << 变量or"字符串"
#include<iostream> //输入输出流头文件 cout标准输出(控制台)和cin标准输入(键盘)
using namespace std; //std标准命名空间
int main()
{
cout<<"Hello world!!!"<<endl; //cout标准输出;endl换行符
return 0;
}
- 使用C++输入输出更方便,不需增加数据格式控制,比如:整形 --> %d,字符 --> %c
数据的输入
作用:用于从键盘获取数据关键字:
cin
语法:
cin >> 变量
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
cin>>a; //cin标准输入
cin>>b>>c;
cout<<"a="<<a<<endl;
cout<<"b="<<b<<" "<<"c="<<c<<endl;
return 0;
}
注意:C++在创建变量时,必须给变量一个初始值,否则会报错
- 这里我们还要注意下cin的特点,他和C语言中的gets有些像,
gets
是遇到换行符
停止,而cin
是以遇到空格
,tab
或者换行符
作为分隔符的,因此这儿输入hello world会被空格符分隔开来。
#include<iostream>
using namespace std;
int main()
{
int a = 1;
float b = 2.1;
double c= 2.111;
char arr[10] = { 0 };
char d[] = "hello world";
cin >> arr;
cout << arr << endl;
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << d << endl;
return 0;
}
这儿我输入
的是hello world
,但因为输入时出现了空格
,所以之后的内容并不会读入,因此arr中存的就是hello。
三、缺省参数
1. 缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参
。
void TestFunc(int a = 0)
{
cout<<a<<endl;
}
int main()
{
TestFunc(); // 没有传参时,使用参数的默认值
TestFunc(10); // 传参时,使用指定的实参
}
2. 缺省参数分类
- 全缺省参数
void TestFunc(int a = 10, int b = 20, int c = 30) //所有参数都设定一个缺省值,
//函数调用时,没有指定的参数则使用缺省值。
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
- 半缺省参数
void TestFunc(int a = 10, int b = 20, int c) //部分参数指定缺省值
//但是指定缺省值的参数必须在参数列表的左边
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
int main()
{
TestFunc(10); //将10给a;b=20;c为任意值:则与C++在创建变量时,必须给变量一个初始值相违背,则报错
}
// 编译器会报错,这是因为半缺省参数从右往左依次来给出,不间隔着给
正确的做法:
#include <iostream>
using namespace std;
void TestFunc(int a , int b = 20, int c = 30) //缺省值必须从左到右
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main()
{
TestFunc(10);
TestFunc(10,11);
TestFunc(10,11,12);
return 0;
}
3. 缺省参数不能在声明的定义中同时出现
//a.h
void TestFunc(int a = 10);
// a.c
void TestFunc(int a = 20)
{}
// 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值
另外,缺省值必须是全局变量
或者常量
。 C语言不支持缺省值。
四、函数重载
1. 函数重载概念(What?)
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数
,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同
,常用来处理实现功能类似数据类型不同的问题。重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,对于程序的可读性有很大的好处。
int Add(int left, int right)
{
return left+right;
}
double Add(double left, double right)
{
return left+right;
}
long Add(long left, long right)
{
return left+right;
}
int main()
{
Add(10, 20); //调用返回值为int的函数
Add(10.0, 20.0); //调用返回值为double的函数
Add(10L, 20L); //调用返回值为long的函数
return 0;
}
2. 为什么需要函数重载(Why?)
- 试想如果没有函数重载机制,如在C中,你必须要这样去做:为这个print函数取不同的名字,如print_int、print_string。这里还只是两个的情况,如果是很多个的话,就需要为
实现同一个功能的函数取很多个名字
,如加入打印long型、char*、各种类型的数组等等。这样做很不友好! 类的构造函数跟类名相同
,也就是说:构造函数都同名
。如果没有函数重载机制,要想实例化不同的对象,那是相当的麻烦!- 操作符重载,本质上就是函数重载,它大大
丰富了已有操作符
的含义,方便使用,如+可用于连接字符串等!
3. 编译器如何解决命名冲突的(How?)
- 在程序进行编译期间,编译器会对函数进行
重命名
,因为c++中有重载的概念,所以编译器在对c和c++中的函数进行重命名时的规则一定不同,下面我们来看看有何不同。 - 首先在属性页中确定生成映射文件(是软件编译后产生的有关用到的所有程序,数据及IO空间的一种映射文件)。编译之后会在项目中的Debug文件中会
生成.map文件
,打开就可以看到编译器为函数进行的重命名。 - 不同的编译器对同一函数重新命名方式不同,但是都是同样的改写规则:“
返回类型+函数名+参数列表
”。
//注意:若仅仅只有返回值不同,其他都相同,则不构成函数重载。
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
既然返回类型也考虑到映射机制中,这样不同的返回类型映射之后的函数名肯定不一样了,但为什么不将函数返回类型考虑到函数重载中呢?——这是为了保持解析操作符或函数调用时,独立于上下文(不依赖于上下文)
,看下面的例子:
float sqrt(float);
double sqrt(double);
void f(double da, float fla)
{
float fl=sqrt(da);//调用sqrt(double)
double d=sqrt(da);//调用sqrt(double)
fl=sqrt(fla);//调用sqrt(float)
d=sqrt(fla);//调用sqrt(float)
}
如果返回类型考虑到函数重载中,这样将不可能再独立于上下文决定调用哪个函数。
- 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
- 另外我们也理解了,为什么函数的返回类型不同,不会构成函数重载,
因为修饰规则并不会受返回值的影响!而跟返回值没关系
extern “C”
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。
extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}
五、引用
1. 引用概念(What?)
引用不是新定义一个变量,而是给已存在变量取了一个别名
,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体;
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
cout<<"a="<<a<<endl;
cout<<"ra="<<ra<<endl;
ra=0;//修改ra的值就相当于修改a的值
cout<<"a="<<a<<endl;
cout<<"ra="<<ra<<endl;
}
注意:引用类型必须和引用实体是同种类型的
- 那么如果引用类型和引用实体是不同种类型的会发生什么?
下面我们来看这么一段代码:
#include<iostream>
using namespace std;
int main()
{
int a = 10;
double& ra = a;
}
在这里int
到double
存在类型的提升,int转成long是向上转换,通常不会有太大问题,而long到int则很可能导致数据丢失,因此要尽量避免后者。
- 但是这个引用对吗?
想要弄明白这个问题,首先要明白隐式类型提升
的问题,在这里int到double存在隐式类型的提升,而在提升的过程中系统会创建一个常量区
来存放a类型提升后的结果。因此到这儿,这段代码一看就是错了,因为你隐式类型提升时a是存放在常量区中的,常量区是不可以被修改的,而你用double&ra
去引用他,ra这个引用是可以被修改的。
- 加个const就可以解决这个问题。
2. 引用的特征
1.引用在定义时必须初始化
//正确示例
int a = 10;
int& b = a;//引用在定义时必须初始化
//错误示例
int a = 10;
int &b;//定义时未初始化
b = a;
2.一个变量可以有多个引用
//相当于有多个“别名”
int a = 10;
int& b = a;
int& c = a;
int& d = a;
3.引用一旦引用了一个实体,就不能再引用其他实体
int a = 10;
int& b = a;
int c = 20;
b = c;//你的想法:让b转而引用c;使得b具有“二义性”,程序报错。
但实际的效果,确实将c的值赋值给b,又因为b是a的引用,所以a的值见解变成了20。
3. 常引用
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;
}
这里的a,b,d都是常量,常量是不可以被修改的
,但是如果你用int&ra等这样来引用a的话,那么引用的这个a是可以被修改的
,因此会出问题。
4. 使用场景
1. 做参数
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
2.引用做返回值
特别注意,我们返回的数据不能是函数内部创建的普通局部变量,因为在函数内部定义的普通的局部变量会随着函数调用的结束而被销毁。我们返回的数据必须是被static修饰或者是动态开辟的或者是全局变量等不会随着函数调用的结束而被销毁的数据。
int& Add(int a, int b)
{
int c=a+b; //出了函数作用域,c不在,回给了系统
return c;
}
int& Add(int a,int b)
{
static c=a+b; //出了函数作用域,c还在,可以用引用返回
return c;
}
举个例子:
#include<iostream>
using namespace std;
int& Add1(int a, int b)
{
int c = a + b; //注意
return c; //随着函数调用的结束而被销毁的数据
}
int& Add2(int a, int b)
{
static int c = a + b; //注意
return c; //不会随着函数调用的结束而被销毁的数据
}
int main()
{
int& ans1 = Add1(1,2);
int& ans2 = Add2(1,2);
cout << ans << endl;
return 0;
}
注意:如果函数返回时,出了函数作用域,返回对象还未还给系统,则可以使用引用返回;如果已经还给系统了,则必须使用传值返回。
5. 引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
return 0;
}
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
cout<<"&a = "<<&a<<endl;
int* pa = &a;
*pa = 20;
cout<<"pa = "<<pa<<endl;
return 0;
}
引用和指针的不同点:
- 引用在定义时
必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
六、内联函数
以inline修饰的函数
叫做内联函数,编译时C++编译器会在调用内联函数的地方展开
,没有函数压栈的开销,内联函数提升程序运行的效率
。
- 普通函数调用:
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用
-
内联函数调用:
-
特性
- inline是一种以
空间换时间
的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。 inline对于编译器而言只是一个建议
,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
七、auto关键字(C++11)
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量
,但遗憾的是一直没有
人去使用它,大家可思考下为什么?
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
。
auto的使用细则
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
cout << x << endl;
cout << a << endl;
cout << b << endl;
cout << c << endl;
*b = 30;
cout << x << endl;
cout << a << endl;
cout << b << endl;
cout << c << endl;
c = 40;
cout << x << endl;
cout << a << endl;
cout << b << endl;
cout << c << endl;
return 0;
}
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
int a[] = {1,2,3};
auto b[] = {4,5,6}; // auto不能直接用来声明数组
}
auto不能直接用来声明数组
八、基于范围的for循环(C++11)
对于一个有范围的集合
而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:
第一部分是范围内用于迭代的变量;
第二部分则表示被迭代的范围。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
范围for的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array) //这里的array其实不是数组,数组在传参时会退化成指针
cout<< e <<endl;
}
九、指针空值nullptr
C++98中的指针空值
在良好的C/C++编程习惯中,在声明一个变量的同时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误。比如未初始化的指针,如果一个指针没有合法的指向,我们基本都是按如下方式对其进行初始化:
int* p1 = NULL;
int* p2 = 0;
NULL其实是一个宏,在传统的C头文件(stddef.h)中可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
#include <iostream>
using namespace std;
void Fun(int p)
{
cout << "Fun(int)" << endl;
}
void Fun(int* p)
{
cout << "Fun(int*)" << endl;
}
int main()
{
Fun(0); //打印结果为 Fun(int)
Fun(NULL); //打印结果为 Fun(int)
Fun((int*)NULL); //打印结果为 Fun(int*)
return 0;
}
程序本意本意是想通过Fun(NULL)调用指针版本的Fun(int* p)函数,但是由于NULL被定义为0
,Fun(NULL)最终调用的是Fun(int p)函数。
注:在C++98中字面常量0,既可以是一个整型数字,也可以是无类型的指针(void*)常量,但编译器默认情况下将其看成是一个整型常量
,如果要将其按照指针方式来使用,必须对其进行强制转换。
C++11中的指针空值
对于C++98中的问题,C++11引入了关键字nullptr。
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为关键字引入的。
- 在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同,大小都为4。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。