通过前面的学习,我们已经掌握了一些最基本的C++入门知识,这一篇博客我们主要聚焦于:C/C++内存管理和C++11的一些新特性,为后续深入学习做好铺垫。
目录
1.2 C语言中动态内存管理方式:malloc/calloc/realloc/free
1.3.2 operator new与operator delete函数(函数的方式申请内存空间)
2.4.1 在C++中遍历一个数组(容器)的方法一般是这样的
2.4.2 在C++11基于范围的for循环(The range-based for statement) ;
一、C/C++内存管理
1.1 C/C++内存分布
当一个程序/进程运行起来后,我们就会有一个虚拟地址空间,我们称之为进程的虚拟地址空间,它又被划分为不同的区域,每一个区域存储不同的数据,如下图所示,可以直观的展示,不同类型变量存放的区域:
【说明】
- 内核:操作系统
- 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口 创建共享共享内存,做进程间通信。
- 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段--存放全局数据和静态数据,分为.bss和.data。
- 代码段--可执行的代码(机器指令)/只读常量。
1.2 C语言中动态内存管理方式:malloc/calloc/realloc/free
【面试题】
1. malloc/calloc/realloc的区别?
2. malloc的实现原理?
1.3 C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因 此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
1.3.1 new运算符的使用
需要注意:new既是一个运算符又是一个关键字。
#include <iostream>
using namespace std;
int main()
{
int n = 10;
int* pa = new int(10);
/* new分配空间经历以下四个步骤:
1、首先通过sizeof计算sizeof(int)空间大小,4个字节 ,就是一个整型变量
2、调用malloc分配内存空间(new的底层还是调用malloc)
3、拿圆括号中的10对分配的整型空间4个字节进行初始化为10
4、将分配的空间的内存地址返回
*/
int* pb = new int[n];
/*new分配空间经历以下四个步骤:
1、首先通过sizeof计算sizeof(int[10])类型的大小,40个字节,相当于10个元素的整型数组
2、调用malloc分配内存空间(new的底层还是调用malloc)
3、没有圆括号,因此它不会进行初始化!!!如果是这种方式:int* pb = new int[n](15),它会将数组的所有元素初始化为15;
4、将分配的空间的内存地址返回
*/
int* pc = new int[n] {1,2,3,4,5,6,7,8};
/*new分配空间经历以下四个步骤:
1、首先通过sizeof计算sizeof(int[10])类型的大小,40个字节,相当于10个元素的整型数组
2、调用malloc分配内存空间(new的底层还是调用malloc)
3、它有花括号,因此它会进行初始化,它会将1 2 3 4 5 6 7 8 对数组进行初始化!
4、将分配的空间的内存地址返回
*/
/*释放内存空间:释放的是堆区分配的那块内存空间,归还给操作系统,从引用状态变成未引用状态,指针变量pa本身没有释放,仍然存放那块内存空间的地址,称之为失效指针*/
delete pa; //释放一个整型变量
delete[] pb; //释放一个数组(多个空间)
delete[] pc; //释放一个数组(多个空间)
/*无论是malloc分配的内存空间还是new分配的,释放后都需要将指针置为空!!!(归还房间的钥匙)*/
pa = NULL;
pb = NULL;
pc = NULL;
return 0;
}
#endif
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用 new[ ]和delete[ ],注意:匹配起来使用。
注意:
- 释放内存空间:释放的是堆区分配的那块内存空间,归还给操作系统,从引用状态变成未引用状态,指针变量pa本身没有释放,仍然存放那块内存空间的地址,称之为失效指针!
- 无论是malloc分配的内存空间还是new分配的,释放后都需要将指针置为空!!!(归还房间的钥匙)
- 如果new申请内存空间失败,并不是像C语言那样返回一个空指针,而是抛出一个异常(C++处理方式):throw std::bad_alloc!不可以用if判断,而是需要用到异常处理try!
int func(int n) { int* p=NULL; try { ip =new int(n); } catch(std::bad_alloc &e) { cout<<e.what()<<endl; } } 但是,如果想用if判断也可以,不让你抛出异常,这样他就会返回空指针(C语言处理方式),如下: int* pa = new(std::nothrow) int(10); if(pa==NULL) { cerr<<"new memory fail"<<endl; return 1; }
1.3.2 operator new与operator delete函数(函数的方式申请内存空间)
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过 operator delete全局函数来释放空间。
#include <iostream>
using namespace std;
int main()
{
int n = 10;
int* pa = new int(10); //运算符的方式使用new(new是一个运算符)
int* pa = (int*)::operator new(sizeof(int)); //函数的方式使用new (new成为一个函数) ,不能进行初始化(和malloc一样)!!但是申请失败,会抛出异常,而不是返回空指针(和new一样)
//int* pa = (int*)malloc(sizeof(int));
int* pb = (int*)::operator new(sizeof(int)*n); //申请连续空间
/*它的释放方式如下:*/
::operator delete(pa); //<==>这里的delete相当于free()
::operator delete(pb);
return 0;
}
operator new 实际也是通过malloc来申请空间,如果 malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
1.3.3 new和delete的实现原理
内置类型:
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是: new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
自定义类型:
new的原理:
- 调用operator new函数申请空间
- 在申请的空间上执行构造函数,完成对象的构造
delete的原理:
- 在空间上执行析构函数,完成对象中资源的清理工作
- 调用operator delete函数释放对象的空间
new T[N]的原理:
- 调用operator new[ ]函数,在operator new[ ]中实际调用operator new函数完成N个对 象空间的申请
- 在申请的空间上执行N次构造函数
delete[ ]的原理:
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
- 调用operator delete[ ]释放空间,实际在operator delete[ ]中调用operator delete来释 放空间。
1.3.4 定位new表达式(placement-new)
#if 0
#include <iostream>
using namespace std;
int main()
{
int n = 10;
int* pa = new int(10);
int* pa = (int*)malloc(sizeof(int)); //申请一个整形空间
int* pb = (int*)::operator new(sizeof(int) * n); //申请连续空间
/*通过上一小节的学习,我们知道这两种方式申请内存空间都未进行初始化,那么定位new就是可以用来进行初始化,使用如下:*/
new(pa) int{ 10 }; //这里是找到pa地址所在空间,然后用10做初始化
new(pb) int[]{1,2,3,4,5,6,7,8,9,10}; //这里是找到pb地址所在空间,然后用1,2,3,4,5,6,7,8,9,10做初始化这个数组
return 0;
}
#endif
#if 0
#include <iostream>
using namespace std;
int main()
{
char buffa[100];
char buffb[100];
//利用new初始化,它不区分数据类型,只要有空间就行,按照给定的值进行初始化
new(buffa) int[]{ 1,2,3,4,5};
new(buffb) double{ 1.1,2.2,3.3,4.3,5.5 };
return 0;
}
#endif
1.4 常见面试题
1.4.1 new ,malloc区别
共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[ ]中指定对象个数即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需 要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成 空间中资源的清理
1.4.2 内存泄漏(面试)
1、什么是内存泄漏,内存泄漏的危害?
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
1.4.3 内存泄漏分类(了解)
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak):
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一 块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分 内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏:
指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
二、 C++11的新特性(部分)
2.1 C++11简介
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了 C++98成为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。 从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于 C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中 约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言, C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更 强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个 重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一一讲解,所以本节主要讲解实际中比较实用的语法。
小故事:
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫 C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。
2.2 类型推导之auto(用的多)
C++11引入了auto和decltype关键字实现类型推导,通过这两个关键字不仅能方便地获取复杂
的类型,而且还能简化书写,提高编码效率。C11中auto成为类型指示符(type-specifier)。
2.2.1 auto类型推导及推导的规则
auto定义的变量,可以根据初始化的值,在编译时推导出变量名的类型。注意:auto 并不能代表一个实际的类型声明, 只是一个类型声明的"占位符"。使用auto声明的变量必须要有初始化值,以让编译器推断出它的实际类型,并在编译时将auto占位符替换为真正的数据类型。
#if 0
#include <iostream>
using namespace std;
int main()
{
int y = 10;
auto x = 10; //根据初始化值10,可以推导出x的类型为:int , auto占位符是: int
auto pa = new auto(1); //根据初始化值1,可以推导出pa的类型为:int * , auto占位符是:int *
auto pb = new auto(12.23); //根据初始化值12.23,可以推导出pb的类型为:double * ,auto占位符是: double *
auto pc = new auto(11.35f); //根据初始化值11.35f,可以推导出pc的类型为:float *, auto占位符是: float *
auto y = 20; //根据初始化值20,可以推导出y的类型为:int , auto占位符是: int
auto *ip = &y, s=100; //根据初始化值&y,可以推导出ip的类型为:int * , auto占位符是: int (*和变量名结合,并不是和auto结合)
auto sp = &y, z=100; //根据初始化值&y,可以推导出sp的类型为:int * , 这里的auto占位符是:int *,后面的z=100会报错!!auto出现二义性!!
return 0;
const int b = 10;
auto p = b; //根据初始化值b,可以推导出p的类型为:int , 而不是const int !!!
auto a = 10; //根据初始化值10,可以推导出a的类型为:int ,那为什么不推导成const int?
auto &c = b; //这里c是b的引用,也就是别名,b是常性的,就可以推导出c的类型为:const int &, 注意:这里不是:int &,因为如果是这个的话,通过c就可以改变b,与定义矛盾!!
}
#endif
#if 0
#include <iostream>
using namespace std;
int main()
{
int x = 10;
auto *ip = &x; //根据初始化值&x,可以推导出ip的类型为:int * , auto占位符是: int
auto xp = &x; //根据初始化值&x,可以推导出xp的类型为:int * , auto占位符是: int *
auto &c = x; //根据初始化值x,可以推导出c的类型为:int & , auto占位符是: int
auto d = x; //根据初始化值x,可以推导出d的类型为:int , auto占位符是: int
const auto e = x; //根据初始化值x,可以推导出e的类型为:const int , auto占位符是: int
auto f = e; //根据初始化值e,可以推导出f的类型为: int
auto &fr = e; //根据初始化值e,可以推导出fr的类型为:const int & , auto占位符是:const int
const auto &cre = e; //根据初始化值e,可以推导出cre的类型为:const int & , auto的类型是:int
/********注意:*和&和变量结合(作为变量的修饰符,修饰的是变量),而不是前面的类型结合***********/
}
#endif
总结:
1、使用auto定义变量的时候,必须要对变量进行初始化,否则编译会报错(如auto y;),因为没有初始值,编译器无法进行推导!
2、C语言中也有auto关键字:auto int s; 这里的auto称之为:自动类型(类型名前进行修饰),C++11中不存在这个自动类型概念,只有类型推导这个概念
3、注意auto的二义性:auto不能同时推导出两个类型!因此,使用auto定义的变量的时候,就不要用逗号隔开了,容易出错!!
4、在不带引用和指针的时候,auto进行推导时忽略CV特性(不考虑const), 在带有引用和指针的时候,auto进行推导时需要考虑CV特性(考虑const)
2.2.2 练习题
#if 0
#include <iostream>
using namespace std;
int main()
{
int a = 10;
const int* ip = &a;
auto p = ip; //根据初始化值ip,可以推导出p的类型为:const int * , auto占位符是:const (有指针的时候需要考虑CV特性)
auto &s = ip; //根据初始化值ip,可以推导出s的类型为:const int *&(常性指针的引用) , auto占位符是:const int *
int* const sp = &a;
auto p2 = sp; //根据初始化值sp,可以推导出p2的类型为: int * const , auto占位符是:int * const (有指针的时候需要考虑CV特性)
auto &p3 = sp; //根据初始化值sp,可以推导出p3的类型为: int *const & , auto占位符是:int * const (有引用的时候需要考虑CV特性)
return 0;
}
#endif
2.2.3 auto作为函数的形参类型
注意:C++20才可以,C++11auto不可以作为函数的形参!
#if 0
#include <iostream>
using namespace std;
void func(auto x)
{
cout<<sizeof(x)<< endl;
cout<<typeid(x).name()<< endl;
}
int main()
{
int a = 10;
int arr[] = {12,23,34,45};
func(a); //4 int
func(arr); //4 int *
}
#endif
#if 0
#include <iostream>
using namespace std;
void func(auto &x)
{
cout << sizeof(x) << endl;
cout << typeid(x).name() << endl;
}
int main()
{
int a = 10;
int arr[] = { 12,23,34,45 };
func(a); //4 int &
func(arr); //16 int [4] &
}
#endif
数组名看成是整个数组的情况:
1、sizeof(arr); 整个数组所占的空间大小(同一个作用域)
2、int (&br)[n]= arr; 数组的引用,引用的是整个数组,右边代表的就是整个数组
3、数组指针:int(*p)[4] =&arr; 这里取出的是整个数组的地址!
2.2.4 auto可以作为函数的返回值类型
auto在作为函数的返回类型时,要保证推导的类型一致!!
auto在作为函数的返回类型时,要保证推导的类型一致!!
#if 0
/*可以使用:auto的占位符是:int*/
auto add(int a, int b)
{
return a + b;
}
/*推导类型不一致,无法使用*/
auto add(int a, int b)
{
if (a == b)
{
return true;
}
else
{
return a + b;
}
}
#endif
#if 0
template<class T>
T my_max(T a, T b)
{
return a > b ? a : b;
}
int main()
{
auto x = my_max(12,23); //auto的占位符是:int
auto y = my_max('a','b'); //auto的占位符是:char
cout << x << endl;
cout << y << endl;
return 0;
}
#endif
2.2.5 auto的限制
#if 0
struct Student
{
auto S_name[20]; //会报错!!auto无法推导结构体成员类型,因此,设计结构体的时候,结构体成员必须明确给出类型!!
int S_age;
};
#endif
#if 0
int main()
{
auto arr[] = { 1,2,3,4,5 }; //会报错!!auto无法推导数组的类型!!
return 0;
}
#endif
#if 0
struct Student
{
char S_name[20];
int S_age;
};
auto func()
{
auto x{ "yhping", 12 }; //会报错!!auto无法推导结构体变量类型!!
return x;
}
#endif
- auto无法推导结构体成员类型,因此,设计结构体的时候,结构体成员必须明确给出类型!!
- auto无法推导数组的类型!!
- auto无法推导结构体变量类型!!
2.3 类型推导之decltype
上一节所讲的auto,用于通过一个表达式在编译时确定待定义的变量类型,auto 所修饰的变量
必须被初始化,编译器需要通过初始化来确定auto所代表的类型,即必须要定义变量。若仅希望得到类型,而不需要(或不能)定义变量的时候应该怎么办呢?C++11新增了decltype关键字,用来在编译时推导出一个表达式的类型。
它的语法格式如下:
decltype (exp), 其中,exp表示一个表达式
从格式上来看,decltype 很像sizeof用来推导表达式类型大小的操作符。类似于sizeof,
decltype的推导过程是在编译期完成的,并且不会真正计算表达式的值。
#if 0
#include <iostream>
using namespace std;
int main()
{
int x = 10;
decltype(x) y; //不用给y初始值,x的类型为整型,则y也就是整型
decltype(x) y=5; //给y一个初始值x的类型为整型,则y也就是整型
decltype(x+y) z; //表达式x+y为整型,所以z也是整型
const int& cr = x;
decltype(cr) i= x; //注意:这里的i必须初始化(定义引用必须初始化)表达式cr的类型为const int &, 所以i也是const int &
int &rx = x;
decltype(rx) ry = x; //注意:这里的ry必须初始化(定义引用必须初始化)表达式rx的类型为 int &, 所以ry也是 int &
return 0;
}
#endif
#if 0
int main()
{
const int a = 10;
decltype(a) b=20; //注意:这里的b必须初始化,表达式a的类型为 const int(常变量定义的时候,必须要初始化) , 所以b也是 const int ,定义常变量必须初始化
return 0;
}
#endif
#if 0
#include <iostream>
using namespace std;
int main()
{
int a = 10; // 声明并初始化整数变量a,值为10。
decltype(++a) ra = a;
/*
因为++a返回的是a自身,decltype(++a)的类型是int&(整型引用),这里的++不会进行,只判断类型
所以 ra 是一个对 a 的引用。int &ra = a;
*/
cout << "a: " << a << endl; //输出a的值,此时a仍然是10,因为++a没有执行。
decltype(a++) rb = a;
/*
因为a++返回的是a的值(将亡值),decltype(a++)的类型是int,这里的++不会进行,只判断类型
所以rb是一个int类型的变量,初始化为a的值。int rb = 10;
*/
cout << "a: " << a << endl; //输出a的值,此时a仍然是10,因为a++没有执行。
//sizeof(++a) 不会实际执行++a操作,所以a的值不会改变。
sizeof(++a);
cout << "a: " << a << endl; //输出a的值此时a仍然是10。
return 0;
}
/*******不论是decltype还是sizeof()是在编译阶段进行类型推导的,但是不会去计算表达式**********/
#endif
2.3.1 函数表达式
decltype可以将函数表达式作为需要推导的结果。
#if 0
#include <iostream>
using namespace std;
int add(int a, int b)
{
return a + b;
}
int main()
{
int x = 10, y = 20;
decltype(add(0, 0)) z; //这里不是调用函数,而是根据函数的返回值类型来确定z的数据类型
z = add(x, y);
return 0;
}
#endif
2.4 基于范围的for循环
在C98中,不同的容器和数组,遍历的方法不尽相同,写法不统一,也不够简洁,而C++11基于范围的for循环以统一、简洁的方式来遍历容器和数组,用起来更方便了。
2.4.1 在C++中遍历一个数组(容器)的方法一般是这样的
#if 0
#include <iostream>
using namespace std;
int main()
{
//下标的方式
int ar[] = { 1,2,3,4,5,6,7,8,9,10 };
int n = sizeof(ar) / sizeof(ar[0]);
for (int i = 0; i < n; ++i)
{
cout << ar[i] << " ";
}
cout<< endl;
//指针的方式
int* ip = NULL;
for (ip = ar; ip != ar + n; ++ip)
{
cout << *ip << endl;
}
cout << endl;
return 0;
}
#endif
2.4.2 在C++11基于范围的for循环(The range-based for statement) ;
以下是基于范围的for循环的一般格式:
- ElemType: 是范围变量的数据类型。它必须与数组(容器)元素的数据类型一样, 或者是数组元素可以自动转换过来的类型。
- val :是范围变量的名称。该变量将在循环迭代期间依次接收数组中的元素值。在第一次循环迭代期间,它接收的是第一个元素的值; 在第二次循环迭代期间,它接收的是第二个元素的值; 以此类推。
- array:是要让该循环进行处理的数组(容器)的名称。该循环将对数组中的每个元素迭代一次。
- statement:是在每次循环迭代期间要执行的语句。要在循环中执行更多的语句,则可以使用一组大括号来包围多个语句。与其他循环体一样,可以用continue来结束本次循环,也可以用break来跳出整个循环。
//容器就是可以存放许多数据的一个对象
#if 0
#include <iostream>
using namespace std;
/******只获取容器中的元素,不会修改容器内的元素**********/
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; //数组就是一个容器
for (int x:arr) //迭代器思想,按照顺序从前向后每次取出数组中的一个元素赋值给x
{
x += 10; //注意这里不会修改数组的元素
cout<< x <<endl;
}
return 0;
}
#endif
#if 0
#include <iostream>
using namespace std;
/******以引用的方式获取容器中的元素,会修改容器内的元素**********/
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; //数组就是一个容器
for (int &x : arr) //迭代器思想,按照顺序从前向后每次取出数组中的一个元素赋值给x
{
x += 10; //注意这里会修改数组的元素,引用的方式来引用数组的元素(可以理解成指针)
cout<< x <<endl;
}
return 0;
}
#endif
2.4.3 可以用auto自动推导出val的数据类型
#if 0
#include <iostream>
using namespace std;
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; //数组就是一个容器
double brr[] = { 11,22,33,44,55,66,77,88 };
for (auto x : arr) //迭代器思想,按照顺序从前向后每次取出数组中的一个元素赋值给x
{
cout << x << endl;
}
for (auto x : brr) //迭代器思想,按照顺序从前向后每次取出数组中的一个元素赋值给x
{
cout << x << endl;
}
return 0;
}
#endif
结合auto,通过下面这种方式,可以打印任意类型的数组:
#if 0
#include <iostream>
using namespace std;
void func(auto &Con) //引用的方式接收整个数组(引用数字)
{
for (auto &x : Con) //这里的Con就是整个数组,他就是个容器
{
cout<< x <<endl;
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; //数组就是一个容器
double brr[] = { 11,22,33,44,55,66,77,88 };
func(arr);
func(brr);
return 0;
}
#endif
2.5 指针空值-- nullptr
2.5.1 问题引入
初始化指针是将其指向一个"空"的位置,比如0x0000 0000。由于大多数计算机系统不允许用户程序写地址为0x0000 0000的内存空间,倘若程序无意中对该指针所指地址赋值,通常在运行时就会导致程序退出。虽然程序退出并非什么坏事,这样一来错误也容易被程序员找到。 因此在大多数的代码中,我们常常能看见需要对传入的指针参数进行判空操作,这样如果传入的是空指针NULL,函数如果进行解引用,程序会崩溃。在C++98标准中,指针初始化的语法如下:
可以看到C++98标准中,NULL被定义为字面常量0,或者是定义为无类型指针(void *) 0常量。不过无论采用什么样的定义,这样我们在使用空指针时,都不可避免地会遇到一些麻烦。先看一个关于函数重载的例子。这个例子我们引用自C++11标准关于nullptr的提案。
会存在一个问题,我们希望的是fun(NULL);调用的是第一个函数,但是编译器总是优先将NULL当作是整型常量0来看待,因此会调用第二个函数,引起该问题的元凶是字面常量0的二义性,在C++98标准中, 字面常量0的类型既可以是一个整型,也可以是一个无类型指针(void* ) 0。如果程序员想在代码清单中调用fun (char * )版本的话,则必须像后面的代码一样,对字面常量0进行强制类型转换( (char*) 0)并调用,否则编译器总是会优先把0看作是一个整型常量。那么C++11就给出了解决办法。
2.5.2 C++11解决办法
在C++11新标准中,出于兼容C++98的考虑,字面常量0的二义性并没有被消除。但标准还是为二义性给出了新的答案,就是nullptr。在C++11标准中,nullptr 是一个所谓'指针空值类型"的常量。指针空值类型被命名为nullptr_ t,事实上,我们可以在支持nullptr的头文件(cstddef) 中找出如下定义:
可以看到,nullptr_ t的定义方式非常有趣,与传统的先定义类型,再通过类型声明值的做法完全
相反(充分利用了decltype的功能)。我们发现,在现有编译器情况下,使用nullptr_t 的时候必须
#include (#include 有些头文件也会间接#include,比如),而nullptr则不用。这大概就是由于
nullptr是关键字,而nullptr_ t是通过推导而来的缘故。
简单而言,由于nullptr是有类型的,且仅可以被隐式转化为指针类型。
2.5.3 使用习惯
可以看到,在改为使用nullptr之后,用户能够准确表达自己的意图,因此,以后我们在书写C++11代码想使用NULL的时候,将NULL 替换成为nullptr我们就能获得更加健壮的代码。
总结:
- nullptr 是C++11新引入的关键字,是一个所谓"指针空值类型"的常量,在C++程序中直接使用。
- 在C++11 中,sizeof(nullptr) 与sizeof(void*)0)所占的字节数相同都( 4,或8)。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
2.6 typedef与using
#include <iostream>
using namespace std;
/*typedef对于一切合法的变量定义在前面加上typedef就会成为类型重命名*/
typedef unsigned char u_int8;
typedef unsigned int u_int32;
typedef int* pint;
typedef const int* cpint;
typedef void (*pfun)(int, int); //函数指针
typedef int arr[10];
int main()
{
arr A, B; //定义两个数组
return 0;
}
#include <iostream>
using namespace std;
/*C++11中的using替代这个typedef*/
using u_int8 = unsigned char;
using u_int32 = unsigned int;
using pint = int*;
using cpint = const int*;
using pfun = void(*)(int, int);
using arr = int [10];
int main()
{
arr A, B; //定义两个数组
return 0;
}
/**********************using的方便使用********************/
#include <iostream>
using namespace std;
template<class T>
using pointer = T*;
int main()
{
pointer<int> p = nullptr;
pointer<doub1e> dp = nullptr;
return 0;
}
2.7 string的简单使用
C语言的字符串& C++的字符串
#include <iostream>
#include <string.h> //c语言中字符串处理函数库
#include <string> //C++中的字符串string类型
using namespace std;
int main()
{
const char* sp = "hello"; //字符串常量
char str1[] = "world"; //字符数组
char str2[20];
// str2= str1; //编译会报错
strcpy(str2, str1);
std::string s0;
std::string s1="abc";
std::string s2{"abc"};
std::string s3={"abc"};
std::string s4={ 'a','b','c'};
cout << s0 << endl; //注意:这里什么也不会打印,C++中定义的字符串会自动加上'\0'
cout << s1 << endl; //abc
cout << s2 << endl; //abc
cout << s3 << endl; //abc
cout << s4 << endl; //abc
return 0;
}
/************string的简单使用************/
#include <iostream>
#include <string.h> //c语言中字符串处理函数库
#include <string> //C++中的字符串string类型
using namespace std;
int main()
{
std::string s1 = { "hello" };
int n = s1.size();//获取字符串的长度,不计算'\0'
for (int i = 0; i < n; i++)
{
cout << s1[i] << " "; //1、以数组方式访问字符串
}
cout << endl;
for (int i = 0; i < n; i++)
{
cout << s1.at(i) << " "; //2、使用字符串方法访问字符串
}
cout << endl;
for (auto& x : s1)
{
cout << x << " "; //3、以范围for的方式访问字符串
}
cout << s1 << endl;
return 0;
}
/******************************************************/
#include <iostream>
#include <string.h> //c语言中字符串处理函数库
#include <string> //C++中的字符串string类型
using namespace std;
int main()
{
std::string s1;
for (int i = 0; i < 26; i++)
{
s1.push_back(i + 'a'); //字符串末尾追加字符
cout << s1 << endl;
}
/*字符串的追加*/
std::string s2{"hello"};
s2.push_back('x');
cout << s2 << endl; //hellox
s2.append("abc");
cout << s2 << endl; //helloxabc
/*更加简单的方式*/
s2 += 'x'; //比原来的方式更加简便
s2 += "abc";
s2 += 23; //这是不可以的!
s2 += to_string(23); //这是可以的, helloxabc23
return 0;
}
至此,C++入门阶段的全部内容就学习完毕,认真复习消化,熟练使用,C++相对来说较为复杂,我们应该时刻理清自己的思路,耐下心来,一点点积累, 星光不问赶路人,加油吧,感谢阅读,如果对此专栏感兴趣,点赞加关注!