C++指针与对象:深入理解指向对象的指针和new的用法
C指针与对象深入理解指向对象的指针和new的用法今天我们来聊一聊C中一个非常重要的话题——指向对象的指针。很多初学者对指针和对象的关系感到困惑尤其是在动态内存管理和对象生命周期方面。本文将通过一个完整的示例程序带你深入理解指向对象的指针、new创建对象、复制构造函数以及指针的常见用法。我们还会简要介绍定位new运算符为你后续的学习打下坚实基础。一、为什么需要指向对象的指针在C中指针不仅可以指向基本类型还可以指向对象。使用对象指针有很多好处动态创建对象可以在运行时决定对象的数量和生命周期。多态性通过基类指针调用派生类的虚函数实现运行时多态。避免对象拷贝传递指针比传递整个对象更高效。跟踪特定对象比如在对象数组中用指针快速指向满足条件的对象。本文将以一个实际程序为例展示如何使用指针来跟踪数组中最短的字符串和按字母顺序排在最前面的字符串。二、示例程序使用指针跟踪字符串我们有一个自定义的String类类似于标准库中的string的简化版它能够动态管理字符串内存。程序允许用户输入最多10条谚语然后找出其中最短的、按字母顺序排在最前面的并随机选择一条作为“最喜欢的谚语”。2.1 String 类简介String类的定义在头文件string1.h中对应书中的程序清单12.4和12.5。它包含一个char*指针str指向动态分配的字符数组。一个int len记录字符串长度。静态成员num_strings统计对象个数。构造函数、析构函数、复制构造函数、重载的赋值运算符、比较运算符等。由于篇幅限制这里不展示完整的类定义我们只需要知道每个String对象都在堆上为自己的字符串数据分配内存并在析构时释放。这是理解后续内存管理的关键。2.2 程序清单修正版下面是原书程序清单12.7的代码我对其中明显的笔误进行了修正例如first sayings;改为first sayings[i];条件判断if (total i)改为if (total 0)#includeiostream#includecstdlib// for rand(), srand()#includectime// for time()#includestring1.h// 自定义String类constintArSize10;constintMaxLen81;intmain(){usingnamespacestd;String name;coutHi, whats your name?\n;cinname;coutname, please enter up to ArSize short sayings empty line to quit:\n;String sayings[ArSize];chartemp[MaxLen];inti;for(i0;iArSize;i){couti1: ;cin.get(temp,MaxLen);while(cincin.get()!\n)continue;if(!cin||temp[0]\0)break;elsesayings[i]temp;// 使用赋值运算符}inttotali;// 实际输入的谚语数量if(total0){coutHere are your sayings:\n;for(i0;itotal;i)coutsayings[i]\n;// 使用指针跟踪最短字符串String*shortestsayings[0];// 使用指针跟踪按字母顺序排在最前面的字符串String*firstsayings[0];for(i1;itotal;i){if(sayings[i].length()shortest-length())shortestsayings[i];// 指向更短的对象if(sayings[i]*first)// 重载的 运算符按字母序比较firstsayings[i];// 指向更靠前的对象}coutShortest saying:\n*shortestendl;coutFirst alphabetically:\n*firstendl;// 随机选择一个谚语并用 new 创建一个新的 String 对象srand(time(0));intchoicerand()%total;String*favoritenewString(sayings[choice]);// 调用复制构造函数coutMy favorite saying:\n*favoriteendl;deletefavorite;// 手动删除动态对象}elsecoutNot much to say, eh?\n;coutBye.\n;return0;}2.3 程序关键点解析① 指针跟踪最短字符串String*shortestsayings[0];for(i1;itotal;i){if(sayings[i].length()shortest-length())shortestsayings[i];}shortest是一个指向String对象的指针初始指向数组第一个元素。在循环中通过shortest-length()访问当前指向对象的length()方法与数组元素sayings[i].length()比较。如果找到更短的就让shortest指向那个元素。注意这里只是改变了指针的指向并没有创建新对象也没有拷贝字符串因此效率很高。② 指针跟踪按字母序最前的字符串String*firstsayings[0];for(i1;itotal;i){if(sayings[i]*first)// 使用重载的 operatorfirstsayings[i];}first同样是指针初始指向第一个元素。sayings[i] *first调用了String类重载的小于运算符通常按字典序比较。如果当前元素比first指向的字符串“小”则更新first指向它。这里需要注意*first是对指针解除引用得到first指向的对象然后与sayings[i]进行比较。③ 使用new创建无名对象String*favoritenewString(sayings[choice]);这行代码做了几件事new String(...)在堆上分配足够存放一个String对象的内存。调用复制构造函数用sayings[choice]初始化这个新对象。返回指向该对象的指针赋值给favorite。这里新创建的对象没有名字只能通过指针favorite来访问。它独立于数组sayings有自己的字符串副本因为复制构造函数会分配新的内存并复制字符串内容。为什么要用new创建新对象因为sayings[choice]是数组中的对象如果直接保存它的指针那么当数组生命周期结束比如离开main函数后该指针将变成野指针。而通过new创建的对象其生命周期由程序员控制直到我们手动delete它。这样即使在程序后续部分比如其他函数中也能安全使用。④ 释放动态对象deletefavorite;这行代码做了两件事调用favorite指向的对象的析构函数释放对象内部str指向的字符串内存。释放用于存放对象本身的内存即str指针和len成员所占的内存。注意这里使用delete而不是delete[]因为favorite指向的是单个对象不是对象数组。三、指针和对象小结通过上面的例子我们可以总结出使用对象指针的几个要点参考图12.53.1 声明指针String*glamour;// 声明一个指向 String 对象的指针目前未初始化3.2 初始化指针指向已有对象String*firstsayings[0];使用new创建新对象并初始化String*favoritenewString(sayings[choice]);// 调用复制构造函数String*gleepnewString;// 调用默认构造函数String*glopnewString(my my my);// 调用带参数的构造函数new与不同的构造函数参数结合可以创建不同初始状态的对象。3.3 通过指针访问成员使用箭头运算符-访问成员函数或数据成员if(sayings[i].length()shortest-length())使用解引用*获得对象本身然后通过点运算符访问成员if(sayings[i]*first)// *first 是对象与 sayings[i] 比较3.4 指针的算术运算虽然例子中没有体现但指向对象的指针也支持算术运算如、--移动的步长等于对象的大小。但通常我们更倾向于使用数组索引除非在底层操作中。3.5 动态对象的销毁对于用new创建的对象必须使用delete手动释放否则会造成内存泄漏。delete会调用析构函数释放对象内部动态分配的资源然后释放对象本身的内存。四、再谈定位 new 运算符除了常规的newC还提供了定位 newplacement new允许在指定的内存地址上构造对象。这在内存池、共享内存等特殊场景中非常有用。4.1 基本用法#includenew// 需要包含头文件charbuffer[1000];// 预先分配的内存缓冲区intmain(){// 在 buffer 地址上构造一个 String 对象初始值为 helloString*pnew(buffer)String(hello);// ... 使用 pp-~String();// 必须显式调用析构函数因为不能使用 delete}4.2 注意事项定位new不分配内存只在指定地址上调用构造函数初始化对象。不能使用delete释放定位new创建的对象因为内存不是由new分配的。必须显式调用析构函数然后由程序员管理原始内存的释放。如果缓冲区是自动变量如栈上的数组则其生命周期结束时自动释放但对象析构仍需手动调用。4.3 与常规 new 的对比特性常规 new定位 new内存来源堆自由存储区程序员指定的任何地址内存分配自动分配需提前分配构造自动调用构造函数自动调用构造函数释放使用delete自动调用析构需显式调用析构函数内存自行释放适用场景绝大多数动态对象创建内存池、共享内存、硬件映射等五、对象内存管理图解为了更直观地理解指针、对象和new的关系我们来看几幅示意图参考原书图12.4~12.6。5.1 指针指向数组中的对象指针shortest和first只是保存了对象的地址它们本身不拥有对象。对象sayings[0]、sayings[1]等存储在栈上因为是局部数组当离开作用域时自动销毁。5.2 使用 new 创建对象new在堆上分配内存并调用构造函数。返回的指针指向这块内存。对象内部可能还有动态分配的内存如String的str指向的字符串这些内存由对象自己管理。当delete指针时先调用析构函数释放内部资源再释放对象本身的内存。5.3 析构函数的调用时机自动对象离开定义它的代码块时调用析构函数。静态对象程序结束时调用析构函数。动态对象new只有delete时才调用析构函数。六、总结通过本文的学习我们掌握了以下要点指向对象的指针可以指向已有对象也可以指向new创建的动态对象。用指针跟踪对象在对象数组中通过指针快速定位满足条件的对象避免拷贝。动态对象的创建与销毁new创建对象delete销毁对象理解构造函数和析构函数的调用过程。复制构造函数的作用当用已有对象初始化新对象时复制构造函数负责深拷贝保证每个对象拥有独立的资源。定位 new 的简单了解在指定内存地址构造对象需要手动管理析构。指针和对象是C面向对象编程的基石深入理解它们对于写出高效、健壮的代码至关重要。希望本文能帮助你理清这些概念并在实际编程中灵活运用。