指针与动态内存分配

指针变量

定义一个变量,程序执行后数据回家再到内存的某个地方,计算机根据其地址来找到该变量

地址:内存按照字节编号,0、1、2、3、4、…

变量的地址:变量占用的首个存储单元的地址

指针变量的使用要点

  • 指针变量在声明时要初始化,否则就成为了野指针
  • 数据类型必须匹配使用,不可以在不同类型指针变量之间赋值
  • 指针变量存储的是某一个数据类型的地址,不可以将一个数据类型直接赋给指针变量

void 指针及强制类型转换

注意:已被淘汰,使用安全性很低了解即可

1
2
3
4
5
6
7
8
int a=7,*pa=&a;
double b=2.5,*pb=&b;

void *pv;
pv=pa;
cout<<*(int*)pv;
pv=pb;
cout<<*(double *)pv<<endl;

任何类型的指针都可以赋值给 void 类型的指针,并且 void 指针不能够查看自身存储的地址,但是经过强制转换为符合的类型是可以查看所储存的地址和地址指向的内容的

指针与一维数组

  • 通过下标访问数组的元素
1
2
3
int a[10];
for(int i=0;i<10;i++)
a[i]=i;
  • 通过数组名访问数组的元素

有一种看法是数组其实就是指针

1
2
3
int a[10];
for(int i=0;i<10;i++)
*(a+i)=i;

数组指针——指向数组的指针

多维数组指针声明方法数据类型 (*指针变量名)[常量表达式]

因为 [] 的优先级要高于 * ,所以要用括号改变优先级结合

1
2
3
4
5
6
7
8
9
10
int a[2][3]={...};
int (*p)[3]=a;//一个指向每个单元包含三个元素的指针

for(int i=0;i<2;i++)
for(int j=0;j<3;j++)
p[i][j]=...;

for(int i=0;i<2;i++)
for(int j=0;j<3;j++)
*(*(p+i)+j)=...;

指针数组——存储指针的数组

多维指针数组声明方法数据类型 变量名[常量表达式]

1
2
3
4
5
6
7
8
9
int a,b,c,d;
//定义含4个元素的指针数组,p只是数组名
int *p[4]={&a,&b,&c,&d};

int aa[2][3];
//pa是指针变量,指向3个int元素的一维数组
int (*pa)[3];

pa=aa;

通过指针访问二维数组的三种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>

signed main()
{
int a[4][4];
int i, j;
int *pInt; //普通指针
int (*pIntPtr)[4]; //数组指针
int *pIntlist[4]; //指针数组

pInt = a[0];
for(int i = 0; i < 4; i++)
pIntlist[i] = a[i];

//指针初始化
pInt = a[0];
pIntPtr = a;
for(int i = 0; i < 4; i++)
pIntlist[i] = a[i];

//通过普通指针初始化二维数组
for(i = 1; i <= 16; i++){
*pInt = i;
pInt++;
}

printf("by pIntPtr:\n");
for(int i = 0; i < 4; i++){
for(j = 0; j < 4; j++){
printf("%3d ", *(*pIntPtr + j));
}
printf("\n");
pIntPtr++;
}

printf("by pIntPtrList:\n");
for(i = 0; i < 4; i++){
for(j = 0; j < 4; j++){
printf("%3d ", *(pIntlist[i] + j));
}
printf("\n");
}
return 0;
}

二级指针

二级指针指的是指向指针的指针,由此指针还可以继续向多级延伸

1
2
3
4
5
int a=5;
int *pa=&a;
int **ppa=&pa;

//a=5 *pa=5 **ppa=5

二级指针经常和指针数组配合使用,因为指针数组内部元素就是一个指针,数组名本身又是一个指针,所以二维指针可以访问指针数组

1
2
3
4
5
6
7
char **p;
char *name[]={"hello","good","world","bye",""};

p=name;
while(**p!=NULL){//判断第一个字符是否为'\0'
cout<<*p++<<endl;
}//字符串的指针数组元素又可以看做是字符数组,所以*p访问整个字符串内容并输出

动态内存分配

程序内部结构

代码块:程序代码,由 exe 文件中相应内容载入后形成,只读

静态数据区:全局变量、静态数据成员和静态局部变量。全局变量、静态数据成员在程序启动后占用存储空间,静态局部变量在第一次执行时创建,如果编程时没有对这些数据初始化,系统将自动初始化为 0

栈区:局部变量、函数参数与返回值,由操作系统自动维护,编程时未初始化将赋予随机值

堆区:由程序员动态请求的数据区,运行时确定,由程序员负责申请和释放,C++ 支持 newdelete 运算符,用于在堆中动态创建对象和释放对象,使用 new 申请内存可能会因为内存不足而失败

动态申请内存方法

1
2
3
4
5
6
7
8
9
10
int *p;
if((p=new int)==NULL){//申请失败退出程序
cout<<"error"<<endl;
exit(0);
}

//申请成功
*p=5;
...
delete p;

exit(o) 、exit(1) 和 return 0 的区别,前者是退出程序,后者是退出函数

1
2
3
4
5
6
7
8
9
10
int *p=new int(5);//分配空间并初始化为5
cout<<*p<<endl;
...
delete p;

int *p=new int[10];//分配10个空间都未初始化
*p=5;
*(p+1)=6;
...
delete []p;

避免内存泄漏

1
2
3
int *pa=new int(5),*pb=new int(10);
pa=pb;
//看似没有问题但是初值为5的空间无法再次访问,也不能delete
1
2
3
4
5
6
7
8
9
10
11
void fun(){
int *pa=new int(5);
...
}

signed main()
{
fun();
//在fun()内申请了栈的空间,但是fun()函数结束释放掉了处在栈区内的指针变量pa也导致了内存泄漏
return 0;
}
  • 不能对动态请求的内存连续使用 delete 释放两次
  • 注意 delete 的本质,释放的是处在栈区的指针变量所指向的堆区内的地址,指针变量本身则在程序结束后由程序自动释放其在栈区占用的空间

释放指针变量pa指向的堆地址的内容后,pa还是指向着那个堆内的地址,只不过内容变了

常指针和指向常量的指针

注意:去别在于 const 和谁结合

  • 常指针const* 后面,说明该指针变量本身是常量,所以指针存储的地址值不能够修改,但是地址指向的内容还是可以修改的
1
2
3
4
5
6
char str1[]{"abcd"};
char * const pc=str1;
pc[2]='a';//对,可以修改

char str2[]{"hello"};
pc=str2;//错,不可以修改
  • 指向常量的指针const* 前面面,说明指针变量本身存储的地址值可以修改,但是指向的地址的内容是常量不可以修改
1
2
3
4
5
6
char str1[]{"abcd"};
char const * pc=str1;//或者const char * pc=str1;
pc[2]='a';//错,不可以修改

char str2[]{"hello"};
pc =str2;//对,可以修改
  • 指向常量的常指针:顾明思议就是上述两种指针的结合,无论是指针本身存储的地址还是地址指向内容都不可以修改
1
2
char str1[]{"abcd"};
char const * const pc=str1;

constexpr 关键字

const 类似,只不过这个是用于指明常量表达式的 constexpression 的缩写

  • 常量表达式:编译是即获得计算结果的表达式
1
2
3
const int max=20;//常量表达式
const int limit=max+1;//由于可以根据上面算出来,常量表达式
const int sz=getSize();//不确定的返还值,非常量表达式

constexpr 声明的变量必须是用常量表达式初始化

1
2
3
constexpr int mf=20;//编译通过
constexpr int limit=mf+1;//编译通过
constexpr int sz=getSize();//编译不通过

编译阶段确定初始值是常量才可以声明为 constexpr ,编译器会在编译阶段进行检查

注意:C++11 之前最后一个是不通过编译的因为变量无法确定初始值,但是 C++11 之后若是将函数声明为 constexpr 则可以通过编译,相当于告诉编译器将函数看做是某个常量

1
2
3
4
5
6
constexpr int square(int x){
return x*x;
}

//C++ 11编译通过
float a[square(9)];//相当于float a(81);

nullptr 空指针常量

旧版本中使用 NULL 和 0 表示空指针,但是这样存在一定的歧义

1
2
3
4
5
void f(int);
void f(char*);

f(0);//重载函数都符合
f(nullptr);//避免了上述的情况,强类型检查

指针及参数传递

函数的值传递

函数的参数时局部变量在栈中,由于调用函数是在不同的作用域所以会发生新的栈的空间申请和变量的复制

1
2
3
4
5
6
7
8
9
int sum(int a,int b){
return a+b;
}

signed main(){
int x(5),y(4);
cout<<sum(x,y)<<endl;
return 0;
}

注意:其实函数的返还机理是函数将 a+b 赋给一个在栈中申请的新的临时变量,然后临时变量返还内容后被释放

函数指针传递

方法:将变量地址传入函数,从而达到间接修改 main() 内的变量

1
2
3
4
5
6
7
8
9
10
void sum(int *a,int b){
*a+=b;
}

signed main(){
int x(5),y(4);
sum(&x,y);
cout<<x<<endl;
return 0;
}

检查自己是否真的了解函数指针传递的机理:

1
2
3
4
5
6
7
8
9
10
11
12
13
void xhg(int *a,int *b){
int temp=*b;
*b=*a;
*a=temp;
}

signed main(){
int x(5),y(4);
xhg(&x,&y);
cout<<x<<" "<<y<<endl;
return 0;
}
//x=4,y=5;

下面的程序 x 和 y 内容没有发生改变而是交换了两个变量存储地址的内容,这是为什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
void xgh(int *a,int *b){
int *temp=b;
b=a;
a=temp;
}

signed main(){
int x(5),y(4);
xhg(&x,&y);
cout<<x<<" "<<y<<endl;
return 0;
}
//x=5,y=4;

因为 temp 也是指针变量,所以起初存储的是 b 地址,当 b 存储的地址赋值为 a 的地址的时候,temp 的地址也跟着发生了改变,所以最终没有成功交换地址的内容,只是 xhg 内的函数的指针变量存储的地址发生了改变

指向函数的指针

理解:应该是可以借助这个指针可以访问多个函数。和数组名一样,函数名就是指向函数的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int add(int a,int b){
return a+b;
}

int sub(int a,int b){
return a-b;
}

int compute(int a,int b,decltype(add) pf){//根据表达式自动判断返还的类型
return pf(a,b);
}

compute(a,b,add);
compute(a,b,sub);

引用的概念

引用是变量或对象别名,建立引用是必须确定引用的对象,对引用的操作实际上就是对被引用者的操作(即引用变量本身不申请额外的空间,引用变量和被建立引用的对象公用的是同一个空间地址)

为什么有指针了还要有引用?实际上引用就是用指针封装实现的,发明出来的原因是减少程序员对指针的恐惧

1
2
int i=1;
int &ri=i;//ri为i的引用,必须初始化

以后对 ri 的操作实际上就是对 i 的操作,可以认为 ri 和 i 在内存中占用的是相同的空间单元

函数的引用传递

引用只是别名,并不位其分配存储单元

1
2
3
4
5
6
7
8
9
10
void sum(int &a,int b){
a+=b;
}

signed main(){
int x(5),y(4);
sum(x,y);
cout<<x<<endl;
return 0;
}

函数返还指针

1
2
3
4
5
6
7
8
9
10
11
12
char * elem(char *s,int n){
return &s[n];
}

signed main(){
char str[]="C++ Program";
char *pc=elem(str,5);//需要定义一个指针变量接受返还值
*pc='A';
cout<<str<<endl;
return 0;
}
//output:C++ PAogram

函数找到修改的地址完后返还一个地址但是会在函数结束的时候销毁,只能main() 内另开一个指针记录下来地址再进行修改

函数返还引用

1
2
3
4
5
6
7
8
9
10
11
char & elem(char *s,int n){
return s[n];
}

signed main(){
char str[]="C++ Program";
elem(str,5)='A';
cout<<str<<endl;
return 0;
}
//output:C++ PAogram

因为返还的类型是主函数 str 引用地址不会销毁,所以直接修改函数返还的就可以

返还引用和指针的陷阱

注意:要格外注意变量的生存周期和所处的作用域

  • 不能向主函数返还指向其他函数局部变量的指针
  • 不能向主函数返还指向其他函数局部变量的引用

因为上述两者都会导致在函数结束时变量被释放,再在主函数内访问就是未知的了

函数递归调用

1
2
3
4
5
//求阶乘
long fact(int n){
if(n==1)return 1;
else return n*fact(n-1);
}

函数的参数值

  • 带缺省值的函数,是一种声明行为,作为不全参数的缺省值
  • 可以在不同的作用域内声明函数的不同缺省值

注意:函数声明内前面放没有指定默认值的参数,从某个位置开始及以后的参数都要含有默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int sum(int a,int b=2,int c=3)

int sum(int a,int b,int c){
return a+b+c;
}

signed main()
{
cout<<sum(2)<<endl;
cout<<sum(2,5)<<endl;
cout<<sum(2,3,6)<<endl;
}
/*output:
7
10
11
*/

内联函数

  • 函数调用便于模块化设计,使程序结构清晰
  • 函数调用占用一定的系统资源:保护现场、参数入栈
  • 作用:将函数的编译后的模块直接嵌入调用处,避免了函数调用的开销
  • 适用于代码量少的函数,且不能为调用函数

尽量减少使用 define

因为宏是在编译之前将源代码进行替换所以没用类型检查

函数重载

  • 多个函数使用相同的函数名但是函数的参数类型或者参数的个数不同
  • 不可以仅仅函数的返还类型不同而进行函数重载
  • 通过重载可以实现对不同类型参数进行调用的统一版本

函数重载注意问题

尽量避免二义性

1
2
int fun(int a,int b=0);
int fun(int x);

可变参数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int sum(initializer_list<int> ls){//可传入未知个数量的int参数
int s{0};
for(auto it=ls.begin(); it!=ls.end(); ++it)
s+=*it;
return s;
}

signed main()
{
ios::sync_with_stdio(false),cin.tie(0);
cout<<sum({3,5,6,2})<<endl;
return 0;
}

STL基础组件要览

string字符串处理

  • C++ 通过 string 封装了字符串的处理
  • 不用担心字符数组的维数以及最后末尾的\0
  • string 对象隐含了其自身的状态信息,封装了大量的操作并且更加安全可靠
  • string 支持标准库的众多算法

string 定义和初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<bits/stdc++.h>
using namespace std;

signed main()
{
string s0;
string s1="hello";
string s2("world");
string s3=s1;
string s4(5,'a');//使用字符'a'重复五次构造
string s5{"hello world"};
return 0;
}

string 基础操作

1
2
3
4
5
6
7
8
9
string s,s1;
cin>>s;//读取到非空字符之前
getline(cin,s);//读取整行
s.size();//长度
s.empty();//判断是否为空
s[n];//指定下标
s+s1;//拼接
s1=s;//复制
==、!=、<、>//string已经重载好运算符

cctype 头文件中存在一系列对于字符的操作函数

截取 string

string s1 = s2.substr(2, 5);

第一个参数内指定截取开始的位置,缺省值为 0 表示从头开始截取,第二个参数指定截取的字符数量(长度),缺省即截取余下的所有字符,若是指定的长度比实际余下的要多就截取余下的所有字符,函数返还新的字符串 stirng

字符串查找

1
2
3
4
5
6
find()//在一个字符串中查找指定的字符或子串,若找到,返回首次匹配的位置,若没找到,返回string::npos
rfind()//与find()类似,只是逆序查找,从串尾查找到串首
find_first_of()//在目标串中查找与指定字符组匹配的第一个字符位置,若未找到,返回npos
find_last_of()
find_first_not_of()//在目标串中查找不匹配指定字符组中任何元素的位置,若找不到则返回npos
find_last_not_of()//

删除字符

s2.erase(2,'a');

第一个参数表示开始删除字符的位置,缺省值即为0,第二个参数表示要删除的字符数,缺省为删除剩下的所有字符,若第二个参数值比实际剩下的字符数多,只删除剩下的所有字符