C++智能指针/普通指针的一些比较

其实想写这个有一段时间了,可能好多写C++许久的人都不敢说自己精通C++指针,因为这里面水很深。自C++11后智能指针的问世更是把指针的使用升了一个层次。根据自己最近的一些应用,想把智能指针和普通指针作个对比,以供后人学习。这里的比较主要是比较指针之间相互赋值所带来的一些问题

把一个指针赋值给另一个指针

首先是普通指针,大家来看一下下面的这个程序,复习一下普通指针

    int x;
    int y;
    int *p = &x;
    int *q = &y;

    x = 35;
    y = 46;

    p = q;      // at this point p is now pointing to the same memory address 
                // as q, both of them are pointing to the memory allocated to y

    *p = 90;    // this will also change the values in *q and y 

    cout << x << " " << y << endl;
    cout << *p << " " << *q << endl;

    return 0;

猜猜看输出结果是什么呢?输出结果是

35 90
90 90

有一个比较有意思的地方是,*q的值改变了,本来之前应该是x即35而现在是90.可以看到我们程序开始把y的地址赋值给了q*q就应该是46了。之后我们没有再对q赋值而是把q赋值给了p。最后我们改变了*p的值没想到也改变了*q的值。原来当程序走过p = q后,pq都指向相同的地址了,改变p指向的值就是改变q指向的值。这个简单的问题竟然在stackoverflow里有了数万的浏览了,见链接
https://stackoverflow.com/questions/7062853/c-pointer-assignment/7062888

使用new初始化普通指针?不要使用new

我想在早期的C++教材或者程序或多或少能看到new的身影,必须要谈及之前的人们为什么要使用它。网上有很多答案,你看了之后觉得懂了,但很可能其实你不懂。最普遍的答案比如new可以让你手动管理内存,使用new初始化了指针之后必须要用delete才能删除指针以及指针指向的内容(大家对边搜为什么要使用new之类的就可以看到类似的答案)。比如下面这个简单的程序

int a = 5;
{
int *p;
int *b = new int;
p = &a;
b = &a;
}

定义了一个普通指针p以及new定义了一个指针b。作为一个局部指针,p在出了大括号{}后就自动消失了。但是b不然,b作为一个new开辟了内存空间的指针,在出了{}之后,它开辟的内存空间(大小为一个int所占的字节)是仍然存在的,如果没有delete,那块内存空间就会一直放在那儿,甚至在程序结束之后都在那儿,一直不能使用了,这就是所谓内存泄漏
但是!!!!!!!
在出了局部{}之后,指针b指向的内容虽然存在,但是指针b已经不可使用了!它的作用域只在{}内。比如你在括号外使用b

int a = 5;
{
int *p;
int *b = new int;
p = &a;
b = &a;
}
std::cout<<*b<<std::endl;//Wrong. b is not defined

程序会告诉你b不存在,没有定义。b相当于就是一个局部指针,它只能在局部范围使用,虽然它开辟的内存空间还在,但是它已经不在了(听起来这么别扭)....你想要在括号外delete它也是不行的。

int a = 5;
{
int *p;
int *b = new int;
p = &a;
b = &a;
}
delete b;//Wrong. b is not defined

当前情况下(后面会讲其他情况),你必须在这个局部范围内把它给delete了。

int a = 5;
{
int *p;
int *b = new int;
p = &a;
b = &a;
delete b;//Correct
}

那么问题来了,既然专门开辟了内存空间的指针也只能作为局部的指针使用,那么我TM为什么要用它???在这个局部范围之内,p指针的作用和它一模一样,出了局部范围,两个指针都不能使用。b指针还必须在局部范围内手动删除,那么我TM为什么要用它???
百度一下关于new的知识,甚至会有错误答案,比如下面这个链接
https://blog.csdn.net/qq_18884827/article/details/52334303
这是我在搜索c++什么时候使用new时的第一个链接,但是很遗憾里面的程序是错误的。它大概说使用了new,指针就可以全局使用了,刚才我们已经说了不可以。它举的例子如下

void func()
{
Student *st=new Student;
st->name="hello";
}
void main()
{
func();
cout<<st->name;//原博主说的`正确的调用`,其实错误
}

但是你随便写一个Student类来补充完整程序,在main函数中调用st程序同样会告诉你st不存在。
但是下面这种情况下,new定义的指针就可以在函数外使用了,我们把程序做了微调并补充完整

class Student{
public:
    std::string name;
};

Student* PrintName(){
    Student *st = new Student;
    st->name = std::string("ABC");
    return st;  //return the pointer
}
int main(){
    Student* st = PrintName();
    std::cout<<st->name<<std::endl;
    delete st;
}

运行这个程序你会发现你可以得到输出ABC。也就是说可以把new定义的指针作为值传递出去。而普通的指针是不行的。

class Student{
public:
    std::string name;
};

Student* PrintName(){
    Student *st; 
    st->name = std::string("ABC");
    return st;  //return the pointer
}
int main(){
    Student* st = PrintName();
    std::cout<<st->name<<std::endl;
}

运行上面这个程序,会出现段错误。new定义的指针确实可以应用到更广的范围,但需要我们上面的程序那样把它"传递"出去。这TM才是我们为什么要用它,并不是可以直接当全局变量。
第二个要用它的原因,很多文章都说了,new是定义了动态分配的内存。比如下面这个百度的回答
https://zhidao.baidu.com/question/553749984.html
它说而一般直接声明数组时,数组大小必须是常量。其实不然。下面这个函数

void func(int i){
     int p[i];
}

是可行的,你可以随时传递一个i进去。也就是在传递进去一个i之前这个数组大小不定。下面这个函数

void func(int i){
     int* p[i];
     //*p[0] = 0;
}

也是可行的。数组指针p的要指的数组的大小一开始也不确定。传入了i之后可以再给指针指向的地址赋值。
所以百度上错误答案或者get不到点的答案还挺多的。我也去StackOverflow上之类的查了一下,很多回答比如
https://stackoverflow.com/questions/655065/when-should-i-use-the-new-keyword-in-c
也切不到关键点。
还有一点我们要注意的是,一旦delete了new定义的指针,它指向的东西也会被销毁。下面的代码可以一目了然

    int a = 5;
    {
    int *p;
    int *b = new int;
    p = &a;
    b = &a;
    delete b;
    }
    std::cout<<a<<std::endl;

运行这个程序,你会得到segmentation fault之类的错误。因为delete b的同时,a也没了,自然不能cout了。
关于new的第一个要使用的原因,其实也有很多代替品。智能指针出现后更是了。所以我们的总结再次强调,C++11后,没有用智能指针的话
不要使用new, 不要使用new, 不要使用new!!!

智能指针赋值

智能指针,自问世以后大量被使用。目前常用的有shared_ptrunique_ptr以及weak_ptr。前面两个使用最多,他们的定义方式如下(整型指针举例)

    std::unique_ptr<int> ptr0 = std::make_unique<int>(); //C++14 以及之后才可以
    std::shared_ptr<int> ptr1 = std::make_shared<int>();//C++11可以
 //or
    std::unique_ptr<int> ptr0(new int);
    std::unique_ptr<int> ptr1(new int);

尽量使用第一种方法,这也是C++官方推荐的。如果要初始化

    std::unique_ptr<int> ptr0 = std::make_unique<int>(5); //C++14 以及之后才可以
    std::shared_ptr<int> ptr1 = std::make_shared<int>(5);//C++11可以
 //or
    std::unique_ptr<int> ptr0(new int(5));
    std::unique_ptr<int> ptr1(new int(5));

这样指针指向的值就是5。weak_ptr的构造方法有些不同,它必须从shared_ptr来创建,有兴趣的同学自己查一下,我也没用过这个指针。
先说一下我使用得最多的shared_ptr。顾名思义,这个指针是可以共享的。所谓共享,就是如果你有两个指针如下

std::shared_ptr<int> ptr0 = std::make_shared<int>();
std::shared_ptr<int> ptr1 = std::make_shared<int>();
int a = 5;
*ptr0 = a; //ptr0 = &a is wrong!!!
ptr1 = ptr0;//share

最后一行使得ptr1和ptr0共享资源ashared_ptr使用计数机制来表明资源被几个指针共享。当我们调用release()时,当前指针会释放资源所有权,计数减1。当计数等于0时,资源会被释放。注意我的注释,虽然仍然是指针,给地址的方法是无法给shared_ptr赋值的。大家可能一时半会儿不能体会到这种共享的强大。我给大家举个简单的栗子。

    std::shared_ptr<int> ptr0 = std::make_shared<int>();
    {
        std::shared_ptr<int> ptr1 = std::make_shared<int>();
        int a = 5;
        *ptr1 = a;
        ptr0 = ptr1;
    }
    std::cout<<*ptr0<<std::endl;

还记得我们之前说的我们简单的使用int* p = new int的这种初始化指针的方法,如果初始化在局部范围,根本不能在局部外使用p。而有了shared_ptr,这个小愿望就可以达成了。上面的程序,我们在局部{}内定义了智能指针ptr1。并把它赋值给了ptr0,此时共享指针计数为2。在局部范围之外,虽然ptr1消失了,a也消失了,此时共享指针的计数为1,但我们仍然可以通过*ptr0获取到之前a储存的值,也就是5。所以我们cout最后能得到5。这就好像,a的肉身(括号之外std::cout<<"a"<<std::endl是不行的)消失了,但是a的灵魂还在(你仍然可以获得5)。这才像"全局"的样子!
即使你new了一个全局的指针和一个局部指针,普通的new能这样吗?不能的。

    int* ptr0 = new int;
    {
        int* ptr1 = new int
        int a = 5;
        ptr1 = &a;
        ptr0 = ptr1;
        delete ptr1; //you have to delete ptr1 here or there will be memory leak!
                          //But then `a` will disappear
    }
    std::cout<<*ptr0<<std::endl;
   delete p1;

正如我注释里所说,你必须在局部把ptr1 delete了。不然就就内存泄露了,但是一旦delete 了ptr1,a就没了,灵魂和肉身都没了...
可能你又想,这有什么用??我遇到过一些情况,我在局部范围内获得了一些点云。不知道点云的朋友没关系,总是就是很大的数据量,而表示点云的库一般大量使用指针/智能指针去表示数据,因为大的数据最好通过指针获得,防止拷贝消耗内存时间等。要把局部的点云传到局部之外,shared_ptr再好用不过了。
unique_ptr其实没什么特别好说的,它作为指针的用处和shared_ptr基本一样,但是顾名思义,是"unique"的。不可拷贝复制。在某些特殊情况下你不希望自己的指针赋值给其他指针,就要用unique_ptr。下面这个程序会编译失败

    std::unique_ptr<int> ptr0 = std::make_unique<int>(5) ;
    std::unique_ptr<int> ptr1 = std::make_unique<int>() ;

    ptr1 = ptr0;//unique_ptr cannot be copied!!!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容