指針(進階講解之一)

之前我們介紹了一下指針的定義和簡單應用,在此基礎上,我們要做出一些進步~

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];   
  • 自增&&加法賦值運算
    • vptr++ => v[1]
    • vptr+=3 => v[3]

在這裏插入圖片描述

  • 自減&&減法賦值運算
    • vptr-- => v[3]
    • vptr-=3 => v[0]

在這裏插入圖片描述
下面看這個厲害的操作:
在這裏插入圖片描述

int x=vptr2-vptr1;
cout<<x;

/*
輸出:2
*/

兩個指針相加得到的偏移量,這就啓發我們get了一個計算數組大小的新思路

需要特別注意的是:兩個指針之間進行加法操作是毫無意義的
  • 賦值運算
int num=1633837924;
int *pNum=&num;
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=&num; 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=&num;

  • 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

那麼你就會很傷心的發現編譯失敗
因爲這時候我們的形參是一個指向常量字符串的指針常量,即指針本身也是一個常量,對於指針我們是不能進行運算操作的

發佈了956 篇原創文章 · 獲贊 211 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章