之前我们介绍了一下指针的定义和简单应用,在此基础上,我们要做出一些进步~
I have a 函数,I have a 指针,啊~然后。。。盘ta!!!
首先,我们需要明确一件很重要的事情:
函数传参数,传指针,传引用到底有什么区别???
传参数
简单的参数传递,相当于将变量copy了一下。传到函数中参与运算
这就像复印文件,不管对复印件进行什么样的骚操,原件始终如一(除非你对原件进行毁灭性破坏【大雾)
可以说,传参数是对主程序变量的一个有效保护
#include<iostream>
using namespace std;
void doit(int n) {
n=n*n;
}
int main() {
int n=5;
doit(n);
cout<<n<<endl;
system("pause");
return 0;
}
/*
输出:5
*/
传指针
当一个函数的参数是指针时,指向的是传入参数的地址
所以函数调用和函数内部使用规则都需要严格遵守指针使用规范
然而这就导致在进行运算时比较繁琐(讲真就是代码比较丑/哭)
#include<iostream>
using namespace std;
void doit(int *p) { //参数变成了int *指针类型
*p=*p * *p; //*p表示取用p指向的变量
}
int main() {
int n=5;
doit(&n); //取地址传入
cout<<n<<endl;
system("pause");
return 0;
}
/*
输出:25
*/
传引用
传引用是C++特有的操作,C中并没有
指针和引用最大的区别:指针开辟了一块内存空间,专门用一块内存存储变量的地址;而引用是个原有的内存空间起一个别名,别名和原名公用一个地址
#include<iostream>
using namespace std;
void doit(int &p) { //传递引用(别名)
p=p*p;
}
int main() {
int n=5;
doit(n);
cout<<n<<endl;
system("pause");
return 0;
}
/*
输出:25
*/
在这里我简单解释一下引用(别名):
int a=5;
int &_a=a;
_a=_a*_a;
cout<<a<<endl;
/*
输出:25
*/
可以看到,在程序中我给 a 起了一个别名 _a (看清楚定义方式哦)
int &_a=a;
要格外注意的是,在定义时就要完成对于_a的初始化
动态分配和释放内存
动态分配——new
在C++程序中,所有内存需求都是在程序执行之前通过定义所需的变量来确定的。但是可能存在程序的内存需求只能在运行时确定的情况。在这些情况下,程序就需要动态分配内存。
在这里我就非常想@那些想要在程序里用变量(非常量)定义数组大小的宝贝们,你们的福音来了~
new 常用格式:
<指针变量>=new<数据类型>
几种使用形式:
- 给单个对象申请分配内存
- 给单个变量申请分配内存的同时初始化该变量
- 同时给多个变量申请分配内存
一定要注意申请的方式哦!!!
int *p; //定义指针
p=new int; //申请指向int类型的指针
*p=9 //初始化
int *p;
p=new int(8); //申请一个指向int类型的指针,变量初始化为8
又来插一句: 括号出现,基本上是三种情况:函数,类(模板),对象(构造函数初始换对象)
这里的括号可以视为是进行对象的初始化
int *p;
p=new int[5]; //申请一个长度为5的int类型数组
//-------------------------------------------
int *p;
int n;
cin>>n;
p=new int[n]; //用变量(非常量)定义数组大小
需要注意的是,用new分配内存时不一定能申请成功
毕竟针对每一个程序计算机分配的内存是有上限的,不可能满足在程序内部无止尽动态申请内存
在以下的实验中,我们不停申请大小为102410244字节的数组,计算机最多允许申请大概400个
#include<iostream>
#include<new> //for "bad_alloc" exception
#include<cstdlib>
using namespace std;
int main()
{
int *ptr,t=0;
try {
while(1) {
ptr=new int[1024*1024];
memset(ptr,0,sizeof(ptr));
cout<<++t<<endl;
}
}
catch (bad_alloc e)
{
cout<<"No sufficient memory to alloc!\n";
exit(1);
}
system("pause");
return 0;
}
释放内存——delete
当程序不再需要由new分配的内存空间时,我们可以使用delete释放这些空间
delete 常用格式:
- delete<指针变量>
- delete[]<指针变量>
int *p;
p=new int;
*p=9;
delete p;
int *p;
p=new int(8);
delete p;
int *p;
p=new int[5];
delete []p;
打起精神来注意啦:
- 用 new 运算符申请分配内存的内存空间,必须用delete释放
- 对于一个已分配内存的指针,只能用 delete 释放一次
- delete 作用的指针对象必须是由 new 分配内存空间的首地址
- 用 new 运算符为多个对象申请内存空间时,不能提供初始化
- 如果指针为 0 ,则对此进行 delete 是合法的
int *p=0; delete p;//okk
- 在 delete 后,被删除指针变为悬空指针,指向曾经存放对象的内存,但是该对象已经不存在
SIZEOF
之前我尝试简单介绍了一下sizeof的用法,那么下面我就尝试着 “ 不简单的 ” 给大家再补充一点
sizeof 是用来计算占用的以字节为单位的内存空间
- 对于常量和变量名,我们呢在使用时是否带括号可以选择:
int number; sizeof(number); //siezof number
- 而对于类型名则必须带括号:
sizeof(char); sizeof(Gradebook);
那么sizeof与指针有什么关系呢?
看下面的程序:
#include<iostream>
using namespace std;
int getSize(double *s) { return sizeof(s);} //int getSize(double s[])
int main()
{
double a[20];
cout<<"The number of bytes in a is "<<sizeof(a)<<endl;
cout<<"The number of bytes returned by getSize is "<<getSize(a)<<endl;
system("pause");
return 0;
}
/*
输出:
The number of bytes in a is 160
The number of bytes returned by getSize is 4
*/
第一个得到的结果是 20(数组大小)* 8(double所占字节)=160
但是第二个sizeof计算的实际上是数组a地址的大小
经过不懈的实验,会发现无论什么类型的数组,得到的结果都是4
(别问,问就是不会)
The number of bytes returned by getSize is 4
指针的运算
指针支持算术,赋值,比较运算
而指针的运算一般与数组结合使用
int v[5]={0};
int *vptr=&v[0];
//int *vptr=v;
最初vptr指向v[0]
我们在这里需要注意一下数组元素的地址,因为int占四个字节,而数组在内存中一定占据一块连续的内存,所以每一位地址在数值上相差4(记录方式采用十六进制)
指针原酸与数值算数运算存在很大区别:
数值运算:
int num=0x00AFFD90; num++; num==0x00AFFD91;
指针运算:
指针vptr指向元素v[0],值为00AFFD90
vptr++运算后,指向v[1],值为00AFFD94
指针运算(假设指向数据类型type的指针)时,+/-n表示前移/后移n个元素,其中n称为offset(偏移值)
从数值上看,指针的值是加/减了n*sizeof(type)
int v[5]={0};
int *vptr1=&v[1];
int *vptr2=&v[3];
下面看这个厉害的操作:
int x=vptr2-vptr1;
cout<<x;
/*
输出:2
*/
两个指针相加得到的偏移量,这就启发我们get了一个计算数组大小的新思路
需要特别注意的是:两个指针之间进行加法操作是毫无意义的
int num=1633837924;
int *pNum=#
char *pChar=(char *)pNum;
cout<<*pChar<<endl;
cout<<pChar<<endl;
cout<<*(++pChar)<<endl; //注意前置自增和后置自增的区别
输出:
d
dcba
c
好啦,隔着屏幕看见你们mengbier的表情后,我来解释一下到底发生了什么:
- 首先,不同类型的指针式之间式不能进行赋值运算的,所以我们强制转换,把int类型的指针转换成了char类型的指针
int num=1633837924;
//int num=0x61626364
- 注意我给出的num,在十六进制下(计算机中地址的记录方式统一使用十六进制)是 0x61626364
我们知道int类型占用四个字节,每个字节是八位二进制,等价于两位十六进制(2^8-1=255)
计算机内部存储时,实际上是把一个二进制数字每八位为一个单位(十六进制每两位为一个单位) 存入字节中,那么61626364这一串数字在计算机中就变成了这样:
|61|62|63|64|
转为十进制:
|97|98|99|100|
在强制转化成字符之后,对应的字符分别为:
|a|b|c|d| - 在转化过程中,字符反序
(为什么?别问,问就是不知道。实践出真知) - *ptr表示当前位的一个元素
通用指针
仅有相同类型的指针之间可以进行赋值运算,特例:通用指针void *
int num=0; int *ptr=# void *p=ptr;
任意类型指针均可以赋值给通用指针,反之不成立
通用指针仅用于保存地址值,不能进行解引用,不能进行算术运算
cout<<*p<<endl; //error C2100:illegal indirection
cout<<p+1<<endl; //error C2036:'void *':nukown size
if (ptr==0) //NULL
cout<<"error"<<endl;
else ...
int a[5]={1,2,3,4,5};
int *p=a;
do {
cout<<*(p++)<<' ';
}
while (p<=&a[4]);
用const修饰指针
一句话——就近原则:
constant pointer:指针常量,指针类型是常量
int * const p;
pointer to constant:常量指针,指向常量的指针
const int *p;
分类 | non-constant data | constant data |
---|---|---|
non-constant pointer | int * p1 | const int * p2 |
constant pointer | int * const p3 | const int * const p4 |
-
p1: Nonconstant Pointer to Nonconstant data
被指向变量的值可以改变,指针的指向也可以改变
-
p2: Nonconstant Pointer to Constant data
常量指针,被指向变量的值不可以改变,但是指针的指向可以改变
-
p3: Constant Pointer to Nonconstant data
指针常量,声明时必须同时进行初始化,或作为形参通过实参初始化
被指向变量的值可以改变,指针的指向不可以改变
int num=100; int * const p=#
-
p4: Constant Pointer to Constant data
指向常量的指针常量,常量指针+指针常量
#include<iostream>
using namespace std;
void printCharacters(const char *ptr) {
for (;*ptr!='\0';ptr++) cout<<*ptr;
//for (int i=0;*(ptr+i)!='\0';i++) cout<<*(ptr+i);
}
int main()
{
//const char phrase[]="print characters of a string";
const char *phrase="print characters of a string";
cout<<"The string is:\n";
printCharacters(phrase);
system("pause");
return 0;
}
在上面这个实验中,我们需要注意以下两种定义方式是不同的:
const char *phrase="print characters of a string";
声明一个指向常量字符串的指针,也就是说指针和字符串分别占据空间
const char phrase[]="print characters of a string";
定义一个常量字符串,占用空间小于上一种定义方式
如果函数的定义变成这样:
void printCharacters(const char * const ptr) {
for (;*ptr!='\0';ptr++) cout<<*ptr;
}
//error C2166:I-value specifies const object
那么你就会很伤心的发现编译失败
因为这时候我们的形参是一个指向常量字符串的指针常量,即指针本身也是一个常量,对于指针我们是不能进行运算操作的