一、指针
指针的基本概念:
1、指针变量:
指针变量:简称指针,它是一种特殊的变量,专门用于存放变量在内存中的起始地址。
语法:数据类型 *变量名 注:*可与数据类型相接,也可以与变量名相接,也可以两者都
不相接,也可以两者都相接,例:char* a; int *a; bool * a; double*a;
四种情况都可以表示指针变量a (一般写作 :例:char* a)。
2、对指针赋值:
用整型指针存放整型变量的地址;用字符型指针存放字符型变量的地址;用浮点型指针存
放浮点型变量的地址,用自定义数据类型指针存放自定义数据类型变量的地址。
语法:指针=&变量名 例:整型变量int a,其存放地址的变量为:int *x = &a,x为任意
名,但不能与a或其他变量相同,即不能重名。其中&的作用为取出变量a的起始地址。
即若输出变量x,就会输出变量a的地址。
注:若指针的数据类型与基类型不符,编译会出现警告,但可以强制转换其类型。
指针指向的诠释:若指针变量存放的是某个对象(这个对象一般来说是内存空间)的地
址,则称这个指针变量指向该对象。
指针加法:将一个整型变量加1后,其值将增加1;但是,将指针(地址的值)加1后,增加的
量等于它指向的数据类型的字节数(与后文一维数组有联系)
3、使用指针
若未给指针赋值则无法使用指针,指针内部未乱七八糟的值,VS会直接报错(其他编译器可
能不报错,但同样无法使用),故使用指针前必须赋初值(为了标准化,建议普通变量也赋
初值,即使只有警告)。
指针存放变量的地址,因此,指针名表示的是地址(就像变量名可以表示变量的值一样)。
*运算符被称为间接值或解除引用(解引用)运算符,将其应用于指针,可以得到该地址的内
存中储存的值,例如:int a=3; int *p=&a; 若输出p,则为a的地址,若输出*p,则输出变量a
储存的值3,以此基础上,可以追加*p=8;即将p所指的地址所储存的值变为8,而p所指的地
址为a的地址,即将a的值重新赋值为8,不再为3。
注意:多个指针可以指向同一个变量,例:int a=3; int *p=&a;int *p1=&a;这样是可行
的,且所指地址相同。
4、const修饰指针
常量指针:
作用:不能通过解引用(即*)的方法修改内存地址中的值(用原始的变量名可以修改)
语法:const 数据类型 *变量名 例: const int *a
注意:
· 指向的变量(对象)可以改变(即之前是指向变量a的,之后可以改为指向变量b)
· 一般用于修饰函数的形参,表示不希望再函数里修改内存地址中的值
· 如果用于形参,虽然指向的对象可以改变,但这么做无意义
范例:int a=3; const int *p=&a; 此处可以使用解引用读取值,即*p=3;但不能使
用*p进行对a值的修改,即:*p=5;此语句是错误的,因为不能给常量赋值,但是可以写
做:a=5,这样是可以的,因为使用的是原始变量名。
指针常量:
作用:指向的变量(对象)不可改变。
语法:数据类型 *const 变量名 例: int a=3; int *const p=&a;
注意:
· 在定义的同时必须初始化,否则没有意义
· 可以通过解引用的方式修改内存地址中的值
范例:int a=3,b=5; int *const p=$a; *p=13; 则a的值变为13,输出*p也为13,但如果
让p重新指向b,则不可行,即追加 p=&b;这行代码,此程序报错
此处埋坑:C++编译器把指针常量做了一些特殊处理,改头换面,作为引用存在,本质
还是指针常量,之后学习到记得在此拉一个跳转。
常指针常量:
作用:指向的变量(对象)不可改变,且不可通过解引用的方法修改内存地址中的值
语法: const 数据类型 *const 变量名
此处埋坑:又名常引用,具体请听下回分晓
总结:
常量指针:指针指向可以改,指针指向的值不可改
指针常量:指针指向不可改,指针指向的值可以改
常指针常量:指针指向不可改,指针指向的值不可改
补充:指针指向的诠释:若指针变量存放的是某个对象(这个对象一般来说是内存空
间)的地址,则称这个指针变量指向该对象。(上方同样诠释,防忘)
5、void关键字
void表示为无类型,主要有三个用途:
1)函数的返回值用void,表示函数没有返回值
void func(int a,int b)
{
函数体代码
return
}
2)函数的参数填void,表示函数不需要参数(或者让函数列表为空)
int fun(void) 或: int fun()
{ {
函数体代码 函数体代码
return 0; return 0;
} }
3)函数的形参用void*,表示接受任意数据类型的指针
只关心地址本身,不关心里面的内容,用void*可以存放任意类型的地址
注意:
· 不能用void声明变量,它不能代表一个真实的变量。例:void a; 错
误,void为一个抽象概念,不是具体的数据类型
· 不能对void*指针直接解引用(需要转换成其他类型的指针)例:
char a = 'X';
void fun(string varname, void* p) //varname为变量名
{
cout << varname << "地址为:" << p << endl; //正确
//cout << varname << "地址为:" << *p << endl; //错误:表达式必须是指向完整对象类型的指针
cout << varname << "地址为:" << *(char*)p << endl; //正确,已经强制类型转换,有完整数据类型char
}
int main()
{
fun("a", &a);
return 0;
}
· 把其他类型的指针赋值给void*指针不需要转换
· 把void*指针赋值给其他类型的指针需要转换
6、二级指针
定义:二级指针用于存放指针变量的地址;详解:指针是指针变量的简称,也是变量,有变
量就有地址。指针用于存放普通变量的地址,二级指针用于存放指针变量的地址。
语法(二级指针):数据类型** 指针名
目的:1)传递地址;2)存放动态分配的内存的地址
在函数中,如果传递普通变量的地址,形参用指针;传递指针地址的地址,形参用二
级指针。
把普通变量的地址从传入函数后可以在函数中修改变量的值;把指针的地址传入函数
后可以在函数中修改指针的值
7、空指针
在C和C++中,用 0 或 NULL 都可以表示空指针。
声明指针后,在赋值之前,让它指向空,表示未指向任何地址。例:int* a=0;或int* a=NULL
空指针使用指南:
1)如果对空指针解引用,程序会崩溃。
2)如果对空指针使用delete运算符,系统会忽略该操作,不会出现异常。所以,内存被
释放后也应该把指针指向空
· 在函数中,应该有判断形参是否为空指针的代码,目的是保证程序的健壮性。
访问空指针异常原因:
NULL指针分配的分区:其范围是从0x00000000到0x0000FFFF。这段空间是空闲的,
对于空闲的空间而言,没有相应的物理储存器与之相对应,所以对这u的那空间来说,
任何读写操作都是会引起异常的。空指针是程序无论在何时都没有物理储存器与之对应
的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,故有上面的
NULL指针分区
C++11的nullptr
用0和NULL表示空指针会产生歧义,C++11建议用nullptr表示空指针,也就是
(void*)0;
NULL在C++中就是0,这是因为在C++中void*类型是不允许隐式转换成其他类型的,所
以之前C++中用0来代表空指针,但是在重载整形的情况下,会出现上述的问题。所
以,C++加入了nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况,
因此,建议用nullptr代替NULL,而NULL就当作0使用。
注意:在Linux平台下,如果使用nullptr,编译需要加-std=c++11参数
8、野指针
野指针就是指针指向的不是一个有效(合法)的地址。在程序中,如果访问野指针,可能会
造成程序的崩溃。
出现野指针的情况:
1)指针在定义的时候,如果没有初始化,它的值是不确定的(乱指一气)。
2)如果用指针指向了动态分配的内存,内存被释放后,指针不会置空,但是,指向的地
址已失效
3)指针指向的变量已超越变量作用域(变量的内存空间按已被系统回收)主要发生在:
调用函数时,让指针指向了函数的局部变量,或者把函数的局部变量的地址作为返回
值赋给了指针
规避方式:
1)指针在定义的时候,如果没地方指,就初始化为nullptr。
2)动态分配的内存被释放后,将其置为 nullptr。
3)函数不要返回局部变量的地址。
注意:野指针的危害比空指针要大很多,在程序中,如果访问野指针,可能会造成程序
的崩溃,是可能,不是一定,程序的表现是不稳定,增加了调试程序的难度。
二、C++内存管理
C++内存模型的基础概念:
1、内存:
分为内核空间和用户空间,内核空间由操作系统管理,与程序员基本无关,程序员代码写在
用户空间,用户空间主要分为四个区:栈,堆,数据段,代码段。
2、栈,堆,数据段,代码段四区具体划分:
栈区:存放了程序中的局部变量、函数参数、返回值;
堆区:存放了程序中的动态开辟内存的变量;
数据段:存放了程序中的全局变量和静态变量;
代码段:存放了可执行变成的二进制代码和常量,程序运行后,代码段中的内容是不会改变的,代
码段数据段很好理解,不细说;
堆和栈:程序处理的数据主要存放在这两个区中 ,栈区效率很高,但空间很小,如果需要处理大
量的数据,就必须
3、堆和栈的主要区别:
1)管理方式不同:栈是编译器自动管理的,在出作用域时,将自动被释放;堆需手动释放,若程
序中不释放,程序结束时由操作系统回收。
2)空间大小不同:堆内存的大小受限于物理内存空间;而栈小的可怜,一般只有8M(可以修改系
统参数)
3)分配方式不同:堆是动态分配;栈由静态分配和动态分配(都是自动释放)
4)分配效率不同:栈是系统提供的数据结构,计算机在底层提供了对栈的支持,进栈和出栈有专
门的指令,效率比较高;堆是由C++函数库提供的;
5)是否产生碎片:对于栈来说,进栈和出栈都有着严格的顺序(先进后出),不会产生碎片;而
堆频繁的分配和释放,会造成内存空间的不连续,易产生碎片,太多的碎片会导致性能下降;
6)增长方式不同:栈向下增长,以降序分配内存地址;堆向上增长,以升序分配内存地址
动态分配内存:
1、实现:new和delete运算符
使用堆区的内存:
1)声明一个指针;
2)用new运算符向系统申请一块内存,让指针指向这块内存;
申请内存语法:new 数据类型(初始值); //C++11支持{}
如果申请成功,返回一个地址;如果申请失败,返回一个空地址(暂不考虑)
注意:new关键字后面的数据类型应和指针的数据类型一样,否则出错:
例:int* p=new int(5);//正确
3)通过对指针解引用的方法,像使用变量一样使用这块内存;
4)如果这块内存不用了,用delete运算符释放它。
释放内存语法:delete 地址;释放内存不会失败(还钱不会失败)
注意:
· 动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据。
· 如果动态分配的内存不用了,必须用delete释放它,否则有可能用尽系统的内存。
· 动态分配的内存生命周期与程序相同,程序退出时若未释放,系统自动回收。
· 就算指针的作用域已经失效,所指向的内存也不会释放。详解:指针归指针,内存归
内存,多个指针可以指向同一块内存。
· 用指针跟踪已分配的内存时,不能跟丢。
三、一维数组和指针
1、数组的地址:
数字在内存中占用的空间是连续的;C++将数组名解释为数组第0哥元素的地址;数组第0个
元素的地址和数组首地址的取值是相同的;数组第n个元素的地址是:数组首地址+n;C++编
译器把数组名[下标] 解释为 *{数组首地址+下标}
2、数组的本质
数组是占用连续空间的一块内存,数组名被解释为数组第0个元素的地址。C++操作这块内存
有两种方法:数组解释法和指针表示法,它们是等价的。
数组解释法:
int a[5] = {3,6,5,8,9};
for(int ii=0;ii<5;ii++)
{
cout << "a" << ii << "的值为" << a[ii] << endl;
}
指针表示法:
int a[5] = {3,6,5,8,9}
int*p =a;
for(int ii=0;ii<5;ii++)
{
cout << "p" << ii << "的值为" << *(p+ii) << endl;
}
3、数组名不一定会被解释为地址
在多数情况下,C++将数组名解释为数组的第0个元素的地址,但是,将sizeof运算符用于数
据名时,将返回整个数组占用内存空间的字节数。
可以修改指针的值,但数组名是常量,不可修改。
4、一维数组作为函数的参数
一维数组作为函数的参数时,只能传数组的地址,并且必须把数组长度也传进去,除非数组
中有最后一个元素的标志
书写方式:void func(int* arr,int len); 或void func(int arr[],int len);
5、用new动态创建一维数组
普通数组再栈上分配内存,栈很小;如果需要存放更多的内存,必须再堆上分配内存
动态创建一维数组的语法:数据类型 *指针=new 数据类型[数组长度]
释放一维数组的语法:delete[] 指针;
注意:
· 动态创建的数组没有数组名,不能用sizeof运算符
· 可以用数组表示法和指针表示法两种方式使用动态创建的数组
· 必须使用delete[]来释放动态数组的内存(不能只用delete,若只用,则只释放首地址
所指的内存空间)
· 不要用delete[]来释放不是new[]分配的内存
· 不要用delete[]释放同一个内存块两次(否则等同于操作野指针)
· 对空指针使用delete[]是安全的(释放内存后,应该把指针置空nullptr)
· 声明普通数组的时候,数组长度可以用变量,相当于在栈上动态创建数组,并且不需
要释放
· 如果内存不足,调用new会产生异常,导致此程序种植;如果在new关键字后面加一
个(std::nothrow)选项,则返回nullptr,不会产生异常
· 系统会自动跟踪已分配数组的尺寸,故使用delete[]释放数组时不需要指定数组大小
三、结构体
1、结构体基本概念
结构体属于用户自定义的数据类型,允许用户储存不同的数据类型
2、结构体定义和使用
语法: struct 结构体名(结构体成员列表)
通过结构体创建变量的三种方法:
· struct 结构体名 变量名
· struct 结构体名 变量名={成员1值,成员2值......}
· 定义结构体时顺便创建变量
举例:
//结构体定义
struct Student
{
string Name;
int Age;
int Score;
}; //注意:此大括号后要加分号
//通过学生类型创建具体学生
struct Student s1; //创建学生s1
struct Student s2={ ... } //创建学生s2,并赋初值
例:
struct Student s2={ s2.Name="BOB"; s2.Age=20; s2.Score=100; }
//定义结构体时顺便定义变量s3
struct Student
{
string Name;
int Age;
int Score;
} s3;