本篇摘要
- 本篇將以最通俗易懂的語言,形象的講述為什么很多情境下,我們優先考慮的
使用指針而不是對象本身
,本篇將給出你答案!
一.從一個生活例子說起,形象秒懂
想象一下,你去圖書館借書,下面你有兩種選擇:
-
把整本書復印一份帶回家,但是,書很厚,復印要時間,占地方,還容易丟。
-
只拿一張“借書卡”,上面寫著書名和位置,而且, 輕便、快速、隨時可以查。
此時我們大多數人就會直接選擇第二種方案了,主打一個通透!
在編程中:
“復印書” = 直接定義對象
Book book; // 在函數里定義,函數結束就沒了
“借書卡” = 指針
Book* ptr = new Book(); // 拿個“卡”,書在別處(堆上)
指針就像“借書卡”——它不存對象本身,只存對象的“地址”。
二·那為啥不直接“看書”,非要用“借書卡”呢(也就是為什么選擇用指針而不是對象呢)?
可以這么認為因為有時候,“直接看書”根本做不到!
下面我們經常下面幾個方面展開敘述:
原因 | 簡要說明 |
---|---|
動態生命周期管理 | 對象可以在運行時創建/銷毀,不受作用域限制 |
多態 | 基類指針可以指向派生類對象 |
避免大對象拷貝 | 指針傳遞比對象拷貝更高效 |
實現復雜數據結構 | 如鏈表、樹、圖等需要指針連接節點 |
延遲初始化 | 對象可以在需要時才創建 |
流程圖效果:
動態生命周期管理
下面舉個通俗易懂例子:
// 對象在棧上,函數結束就銷毀
void badExample() {MyClass obj;// obj 在函數返回時自動析構
}// 指針可以控制對象生命周期
MyClass* ptr = new MyClass();
delete ptr; // 手動控制銷毀
-
可以看出這里如果使用指針的生命周期是由我們自己控制的,不受作用域限制!
-
適用于:數據庫連接、網絡套接字、單例等需要跨函數/模塊存在的對象。
多態
下面從我們最熟悉的繼承多態來分析下:
- 比如我們如果想寫代碼的時候,當描述的對象有些相似的特征,我們就會考慮到進行繼承多態來簡化操作,便于管理,因此這里的基類指針就是我們必不可少的了!
#include<iostream>
using namespace std;
class Animal {
public:virtual void speak() = 0;
};class Dog : public Animal { void speak() override { cout << "我是一只狗,我要叫了:"<<"汪\n"; } };
class Cat : public Animal { void speak() override { cout << "我是一只貓,我要叫了:"<<"喵\n"; } };int main(){
// 基類指針可以指向任意派生類
Animal* animals[2];
animals[0] = new Dog();
animals[1] = new Cat();for (int i = 0; i < 2; ++i) {animals[i]->speak();
}}
效果展示:
-
看到這就是我們非常親切的多態效果了!
-
這是直接定義對象無法實現的!
避免大對象拷貝
這里,我們回憶下,通常比如函數傳參的時候用的要么是對象,指針,引用,而這里我們重點看對象和指針的區別。
下面先看下例子:
class BigObject
{char data[1024 * 1024]; // 1MB
};// 每次傳參都會拷貝 1MB 內存
void process1(BigObject obj)
{cout << "對象" << endl;
};// 指針只傳 8 字節地址
void process(BigObject *ptr)
{cout << "指針" << endl;
};void process2(BigObject&a){
cout << "引用" << endl;}int main()
{BigObject *p = nullptr;int s1=clock();process(p); // 指針int e1=clock();BigObject b;int s2=clock();process1(b); // 對象int e2=clock();int s3=clock();process2(b);int e3=clock();cout<<"指針耗時:"<<e1-s1<<" 消耗內存: "<<sizeof( BigObject *)<<endl;cout<<"對象耗時:"<<e2-s2<<" 消耗內存: "<<sizeof( BigObject )<<endl;cout<<"引用耗時:"<<e3-s3<<endl;}
運行效果:
當然這里每次時間可能不同,這里忽視,我們可以看到對象耗時是最長的,其次是指針,而引用達到了最快。
下面解釋下原因:
- 對象:需要拷貝構造,消耗一個對象大小,故耗時耗內存。
- 指針:需要構建指針,故消耗指針大小。
- 引用:直接起別名,幾乎不占內存,由編譯器在編譯期處理別名關系。
因此,這里如果條件符合,一定是選擇指針比較優的(如果對象特別大,對象的話,這不就是自己給自己找麻煩)!
實現復雜數據結構
想做個“鏈表”或“樹”,比如圖論的一些算法等具有連接關系的模型,都是需要指針來解圍的(這里順便推薦下博主的圖論專欄,講的超級詳細:圖論專欄)。
比如你要做微信好友關系鏈:
struct Person {string name;Person* friend; // 指向下一個好友
};
沒有指針,你怎么表示“張三的好友是李四”?
這里我們經常設置進去的是指針來表示這種關系,似乎都已經成為常態了!
延遲初始化
#include <iostream>
#include <ctime>
using namespace std;
class Animal
{
public:virtual void speak() = 0;
};class Dog : public Animal
{void speak() override { cout << "我是一只狗,我要叫了:" << "汪\n"; }
};
class Cat : public Animal
{void speak() override { cout << "我是一只貓,我要叫了:" << "喵\n"; }
};class Player
{
public:Animal *Animaler = nullptr;void start(){Animaler = new Dog(); // 按需創建}
};
int main()
{Player p;//此時沒有Animaler指向的對象,也就是沒有對象產生p.start();//此時才構造處對象p.Animaler->speak();
}
效果展示:
- 對象只在真正需要時才分配內存。,真正做到了方便,節省資源!
四·直接定義對象的優勢
下面看下優勢總結:
優點 | 說明 |
---|---|
自動管理內存 | RAII,作用域結束自動析構 |
性能更高 | 無間接訪問開銷 |
更安全 | 不會空指針、內存泄漏(如果不用 new) |
比如:
void goodExample() {MyClass obj; obj.doWork();
} // 自動調用 ~MyClass()
-
當我們使用對象的時候(非new),它會自己出了作用域自動析構,安全感拉滿,但是new了就需要手動delete否則內存泄漏!
-
能用棧對象就用棧對象!
五· 普通指針的壞處
指針有沒有壞處?當然有!指針就像“雙刃劍”:
- 優點:靈活、高效、支持多態 。
- 缺點:容易空指針、內存泄漏、野指針。
存在隱患:
非法訪問:
Book* ptr = nullptr;
ptr->read(); // 空指針崩潰!段錯誤!
忘記手動釋放:
Book* ptr = new Book();
// 忘了 delete ptr; // 內存泄漏!
六· 現代C++怎么解決這些問題?——“智能指針”
別怕,C++11 以后有了“智能指針”,它像“自動還書機”:
#include <memory>
// 自動管理內存,不用手動 delete
unique_ptr<Book> ptr = make_unique<Book>();
ptr->read(); // 正常使用
// 函數結束,自動釋放內存,安全又省心
-
這就類似我們普通指針,賦能添加了自動delete工作!
-
有了智能指針,從此麻麻再也不怕我用指針操作,忘記delete了!
七· 何時使用指針 vs 直接定義對象?
下面基于上文所有的舉例以及博主總結得到下面的使用推薦方法:
場景 | 推薦方式 | 備注 |
---|---|---|
小對象,函數內使用 | MyClass obj; | 棧分配,自動管理生命周期 |
需要多態(貓/狗) | Base* ptr = new Derived(); | 需配合delete 手動釋放 |
大對象,避免拷貝 | func(Object* ptr) | 指針傳遞避免拷貝開銷 |
動態結構(鏈表) | Node* next | 典型指針鏈接結構 |
現代C++項目 | unique_ptr/shared_ptr | 優先使用智能指針管理資源 |
八· 總結
用指針不是因為“高級”,而是因為“需要”,指針不是“炫技”,而是為了解決實際問題而存在的工具。
記住:
能不用指針就不用,要用就用智能指針。
如果你覺得這篇文章幫你理清了思路,歡迎點贊、收藏、轉發!
歡迎在評論區留言:
“我以前一直搞不懂指針,現在終于明白了!”