13b3ebbe14
把川大 wiki「02.Qt方向」课程代码整理为 CMake 管理的可编译运行项目。
Part1 C/C++ 强化 (p01/, 144 目标, host gcc/g++ 构建, 全绿):
- 修复全部编译失败:补 <cstring>/<fstream>/<iomanip>/<vector> 等缺失头
(common_fix + infer_stl_includes 自动检测注入)、修正 pause() 壳注入
(改检测已转换后的 pause 调用)、namespace 提升出函数体、const 成员函数
正确性、dedupe 不再破坏函数重载集、K&R 隐式 int / 动态异常规范等
C++17 移除特性的可编译等价改写。
- 目录采用深缩写 p01/ch04/s03/ (原 part1_cpp/ch04_class_object/03_ctor_dtor)。
Part3 Qt 桌面 (p03/, 29 目标, 隔离 Qt 5.14.2 构建, 全绿):
- gen_part3.py: Qt 外壳模板 (main+QApplication+show, QTimer 自动退出便于
offscreen 验证)、AUTOMOC + .moc include、rewrite_* 处理散文混入/重载/
资源依赖、生成占位 png/gif + .qrc + rundata 样例数据。
- 修正 QLable 笔误、全角分号、.→->、Windows 路径、QApplication 阻塞事件
循环改为 QCoreApplication 等。
任务1 概念/版本验证:
- tools/std_probe.py: 跨 -std= 编译探针 (--pedantic-errors 让已废除特性硬失败)。
- tools/demo_versions/: K&R 隐式 int、三目左值、void* 转换、enum 赋整、
const 经指针、动态异常规范 共 6 个跨版本演示。
- docs/VERSION_NOTES.md: 每条「原文说法→C17/C++17 是否成立→何版本成立→已验证」。
文档: 顶层 README、docs/SOURCE_PAGES.md (页面→目录映射)、
docs/ERRATA.md (Part1 修正)、docs/ERRATA_part3.md (Part3 修正)。
另修复 tools/run.sh / run_qt.sh 的 ${1:?...} 引号语法 bug。
隔离 Qt 验证: ldd 0 系统 Qt 引用; 173/173 目标全绿; p03 offscreen 运行通过。
1 line
37 KiB
JSON
1 line
37 KiB
JSON
{"id":"58954451","type":"page","status":"current","title":"03.对象的构造和析构","version":{"by":{"type":"known","username":"ethanliu","userKey":"2c90931b6cfb654f016d5c69becc0001","profilePicture":{"path":"/images/icons/profilepics/default.svg","width":48,"height":48,"isDefault":true},"displayName":"刘浩","_links":{"self":"https://wiki.suncaper.net/rest/api/user?key=2c90931b6cfb654f016d5c69becc0001"},"_expandable":{"status":""}},"when":"2022-07-09T01:51:24.000Z","message":"","number":1,"minorEdit":false,"hidden":false,"_links":{"self":"https://wiki.suncaper.net/rest/experimental/content/58954451/version/1"},"_expandable":{"content":"/rest/api/content/58954451"}},"body":{"storage":{"value":"<ac:layout><ac:layout-section ac:type=\"two_right_sidebar\"><ac:layout-cell><h2><span>4.3 </span>对象的构造和析构</h2><h3><span>4.3.1 </span>初始化和清理</h3><p>我们大家在购买一台电脑或者手机,或者其他的产品,这些产品都有一个初始设置,也就是这些产品对被创建的时候会有一个基础属性值。那么随着我们使用手机和电脑的时间越来越久,那么电脑和手机会慢慢被我们手动创建很多文件数据,某一天我们不用手机或电脑了,那么我们应该将电脑或手机中我们增加的数据删除掉,保护自己的信息数据。</p><p>从这样的过程中,我们体会一下,所有的事物在起初的时候都应该有个初始状态,当这个事物完成其使命时,应该及时清除外界作用于上面的一些信息数据。</p><p>那么我们<span>c++</span>中<span>OO</span>思想也是来源于现实,是对现实事物的抽象模拟,具体来说,当我们创建对象的时候<span>,</span>这个对象应该有一个初始状态,当对象销毁之前应该销毁自己创建的一些数据。</p><p>对象的初始化和清理也是两个非常重要的安全问题,一个对象或者变量没有初始时,对其使用后果是未知,同样的使用完一个变量,没有及时清理,也会造成一定的安全问题。<span>c++</span>为了给我们提供这种问题的解决方案,<strong>构造函数</strong>和<strong>析构函数</strong>,这两个函数将会被编译器自动调用,完成对象初始化和对象清理工作。</p><p><strong>无论你是否喜欢,对象的初始化和清理工作是编译器强制我们要做的事情,即使你不提供初始化操作和清理操作,编译器也会给你增加默认的操作,只是这个默认初始化操作不会做任何事,所以编写类就应该顺便提供初始化函数。</strong></p><p>为什么初始化操作是自动调用而不是手动调用?既然是必须操作,那么自动调用会更好,如果靠程序员自觉,那么就会存在遗漏初始化的情况出现。</p><h3><span>4.3.2 </span>构造函数和析构函数</h3><p>构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。</p><p>析构函数主要用于对象<strong>销毁前</strong>系统自动调用,执行一些清理工作。</p><p><strong>构造函数语法:</strong></p><table class=\"wrapped\"><colgroup><col /></colgroup><tbody><tr><td><ul><li> 构造函数函数名和类名相同,没有返回值,不能有<span>void</span>,但可以有参数。</li><li><span>ClassName(){}</span></li></ul></td></tr></tbody></table><p><span> </span></p><p><strong>析构函数语法:</strong></p><table class=\"wrapped\"><colgroup><col /></colgroup><tbody><tr><td><ul><li>析构函数函数名是在类名前面加”<span>~</span>”组成<span>,</span>没有返回值,不能有<span>void,</span>不能有参数,不能重载。</li><li>~ClassName(){}</li></ul></td></tr></tbody></table><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"18e960db-7801-4ce9-8100-20472b08631e\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n\tPerson(){\n\t\tcout << \"构造函数调用!\" << endl;\n\t\tpName = (char*)malloc(sizeof(\"John\"));\n\t\tstrcpy(pName, \"John\");\n\t\tmTall = 150;\n\t\tmMoney = 100;\n\t}\n\t~Person(){\n\t\tcout << \"析构函数调用!\" << endl;\n\t\tif (pName != NULL){\n\t\t\tfree(pName);\n\t\t\tpName = NULL;\n\t\t}\n\t}\npublic:\n\tchar* pName;\n\tint mTall;\n\tint mMoney;\n};\n\nvoid test(){\n\tPerson person;\n\tcout << person.pName << person.mTall << person.mMoney << endl;\n}]]></ac:plain-text-body></ac:structured-macro><h3><span>4.3.3 </span>构造函数的分类及调用</h3><ul><li>按参数类型:分为无参构造函数和有参构造函数</li><li>按类型分类:普通构造函数和拷贝构造函数<span>(</span>复制构造函数<span>)</span></li></ul><p><br /></p><p><br /></p><p><br /></p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"03fb4f94-63be-40ae-a5b3-caa77de460a2\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n\tPerson(){\n\t\tcout << \"no param constructor!\" << endl;\n\t\tmAge = 0;\n\t}\n\t//有参构造函数\n\tPerson(int age){\n\t\tcout << \"1 param constructor!\" << endl;\n\t\tmAge = age;\n\t}\n\t//拷贝构造函数(复制构造函数) 使用另一个对象初始化本对象\n\tPerson(const Person& person){\n\t\tcout << \"copy constructor!\" << endl;\n\t\tmAge = person.mAge;\n\t}\n\t//打印年龄\n\tvoid PrintPerson(){\n\t\tcout << \"Age:\" << mAge << endl;\n\t}\nprivate:\n\tint mAge;\n};\n//1. 无参构造调用方式\nvoid test01(){\n\t\n\t//调用无参构造函数\n\tPerson person1; \n\tperson1.PrintPerson();\n\n\t//无参构造函数错误调用方式\n\t//Person person2();\n\t//person2.PrintPerson();\n}\n//2. 调用有参构造函数\nvoid test02(){\n\t\n\t//第一种 括号法,最常用\n\tPerson person01(100);\n\tperson01.PrintPerson();\n\n\t//调用拷贝构造函数\n\tPerson person02(person01);\n\tperson02.PrintPerson();\n\n\t//第二种 匿名对象(显示调用构造函数)\n\tPerson(200); //匿名对象,没有名字的对象\n\n\tPerson person03 = Person(300);\n\tperson03.PrintPerson();\n\n\t//注意: 使用匿名对象初始化判断调用哪一个构造函数,要看匿名对象的参数类型\n\tPerson person06(Person(400)); //等价于 Person person06 = Person(400);\n\tperson06.PrintPerson();\n\n\t//第三种 =号法 隐式转换\n\tPerson person04 = 100; //Person person04 = Person(100)\n\tperson04.PrintPerson();\n\n\t//调用拷贝构造\n\tPerson person05 = person04; //Person person05 = Person(person04)\n\tperson05.PrintPerson();\n}\n\n]]></ac:plain-text-body></ac:structured-macro><h3><span>4.3.4 </span>拷贝构造函数的调用时机</h3><ul><li>对象以值传递的方式传给函数参数</li><li>函数局部对象以值传递的方式从函数返回<span>(vs debug</span>模式下调用一次拷贝构造,<span>qt</span>不调用任何构造<span>)</span></li><li>用一个对象初始化另一个对象</li></ul><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"86835dc1-41f9-4790-b020-e0834477d534\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n\tPerson(){\n\t\tcout << \"no param contructor!\" << endl;\n\t\tmAge = 10;\n\t}\n\tPerson(int age){\n\t\tcout << \"param constructor!\" << endl;\n\t\tmAge = age;\n\t}\n\tPerson(const Person& person){\n\t\tcout << \"copy constructor!\" << endl;\n\t\tmAge = person.mAge;\n\t}\n\t~Person(){\n\t\tcout << \"destructor!\" << endl;\n\t}\npublic:\n\tint mAge;\n};\n//1. 旧对象初始化新对象\nvoid test01(){\n\n\tPerson p(10);\n\tPerson p1(p);\n\tPerson p2 = Person(p);\n\tPerson p3 = p; // 相当于Person p2 = Person(p);\n}\n\n//2. 传递的参数是普通对象,函数参数也是普通对象,传递将会调用拷贝构造\nvoid doBussiness(Person p){}\n\nvoid test02(){\n\tPerson p(10);\n\tdoBussiness(p);\n}\n\n//3. 函数返回局部对象\nPerson MyBusiness(){\n\tPerson p(10);\n\tcout << \"局部p:\" << (int*)&p << endl;\n\treturn p;\n}\nvoid test03(){\n\t//vs release、qt下没有调用拷贝构造函数\n\t//vs debug下调用一次拷贝构造函数\n\tPerson p = MyBusiness();\n\tcout << \"局部p:\" << (int*)&p << endl;\n}]]></ac:plain-text-body></ac:structured-macro><p><span> </span></p><table class=\"wrapped\"><colgroup><col /></colgroup><tbody><tr><td><p> <strong>[Test03</strong><strong>结果说明<span>:]</span></strong></p><p> 编译器存在一种对返回值的优化技术<span>,RVO(Return Value Optimization).</span>在<span>vs debug</span>模式下并没有进行这种优化,所以函数<span>MyBusiness</span>中创建<span>p</span>对象,调用了一次构造函数,当编译器发现你要返回这个局部的对象时,编译器通过调用拷贝构造生成一个临时<span>Person</span>对象返回,然后调用<span>p</span>的析构函数。</p><p> 我们从常理来分析的话,这个匿名对象和这个局部的<span>p</span>对象是相同的两个对象,那么如果能直接返回<span>p</span>对象,就会省去一个拷贝构造和一个析构函数的开销,在程序中一个对象的拷贝也是非常耗时的,如果减少这种拷贝和析构的次数,那么从另一个角度来说,也是编译器对程序执行效率上进行了优化。</p><p> 所以在这里,编译器偷偷帮我们做了一层优化:</p><p> 当我们这样去调用<span>: Person p = MyBusiness();</span></p><p> 编译器偷偷将我们的代码更改为<span>:</span></p></td></tr></tbody></table><p><span> </span></p><h3><span>4.3.5 </span>构造函数调用规则</h3><ul><li>默认情况下,<span>c++</span>编译器至少为我们写的类增加<span>3</span>个函数<ul><li>默认构造函数<span>(</span>无参,函数体为空<span>)</span></li><li>默认析构函数<span>(</span>无参,函数体为空<span>)</span></li><li>默认拷贝构造函数,对类中非静态成员属性简单值拷贝</li></ul></li><li>如果用户定义拷贝构造函数,<span>c++</span>不会再提供任何默认构造函数</li><li>如果用户定义了普通构造<span>(</span>非拷贝<span>)</span>,<span>c++</span>不在提供默认无参构造,但是会提供默认拷贝构造</li></ul><h3><span>4.3.6 </span>深拷贝和浅拷贝</h3><p><span>4.3.6.1 </span>浅拷贝</p><p>同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种情况被称为<strong>浅拷贝<span>.</span></strong></p><p>一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。</p><p><ac:image ac:height=\"250\"><ri:attachment ri:filename=\"image2021-5-8_16-45-18.png\" /></ac:image></p><p><span>4.3.6.2 </span>深拷贝</p><p>当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间,深拷贝。</p><p><ac:image ac:height=\"250\"><ri:attachment ri:filename=\"image2021-5-8_16-45-44.png\" /></ac:image></p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"8f000ca1-ac0d-4432-8a0d-314fef0820e0\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n\tPerson(char* name,int age){\n\t\tpName = (char*)malloc(strlen(name) + 1);\n\t\tstrcpy(pName,name);\n\t\tmAge = age;\n\t}\n\t//增加拷贝构造函数\n\tPerson(const Person& person){\n\t\tpName = (char*)malloc(strlen(person.pName) + 1);\n\t\tstrcpy(pName, person.pName);\n\t\tmAge = person.mAge;\n\t}\n\t~Person(){\n\t\tif (pName != NULL){\n\t\t\tfree(pName);\n\t\t}\n\t}\nprivate:\n\tchar* pName;\n\tint mAge;\n};\n\nvoid test(){\n\tPerson p1(\"Edward\",30);\n\t//用对象p1初始化对象p2,调用c++提供的默认拷贝构造函数\n\tPerson p2 = p1;\n}]]></ac:plain-text-body></ac:structured-macro><h3><span>4.3.7 </span>多个对象构造和析构</h3><h4><span>4.3.7.1 </span>初始化列表</h4><p>构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。</p><p>初始化列表简单使用<span>:</span></p><p><br /></p><p><br /></p><p><br /></p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"96e93e81-cdbe-4fa1-83a2-1ac3a628b90d\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n#if 0\n\t//传统方式初始化\n\tPerson(int a,int b,int c){\n\t\tmA = a;\n\t\tmB = b;\n\t\tmC = c;\n\t}\n#endif\n\t//初始化列表方式初始化\n\tPerson(int a, int b, int c):mA(a),mB(b),mC(c){}\n\tvoid PrintPerson(){\n\t\tcout << \"mA:\" << mA << endl;\n\t\tcout << \"mB:\" << mB << endl;\n\t\tcout << \"mC:\" << mC << endl;\n\t}\nprivate:\n\tint mA;\n\tint mB;\n\tint mC;\n};\n\n]]></ac:plain-text-body></ac:structured-macro><p><strong>注意:</strong>初始化成员列表<span>(</span>参数列表<span>)</span>只能在构造函数使用。</p><h4><span>4.3.7.2 </span>类对象作为成员</h4><p>在类中定义的数据成员一般都是基本的数据类型。但是类中的成员也可以是对象,叫做<strong>对象成员</strong>。</p><p>C++中对对象的初始化是非常重要的操作,当创建一个对象的时候,<span>c++</span>编译器必须确保调用了所有子对象的构造函数。如果所有的子对象有默认构造函数,编译器可以自动调用他们。但是如果子对象没有默认的构造函数,或者想指定调用某个构造函数怎么办?</p><p>那么是否可以在类的构造函数直接调用子类的属性完成初始化呢?但是如果子类的成员属性是私有的,我们是没有办法访问并完成初始化的。</p><p>解决办法非常简单:对于子类调用构造函数,<span>c++</span>为此提供了专门的语法,即构造函数初始化列表。</p><p>当调用构造函数时,首先按各对象成员在类定义中的顺序(和参数列表的顺序无关)依次调用它们的构造函数,对这些对象初始化,最后再调用本身的函数体。也就是说,先调用对象成员的构造函数,再调用本身的构造函数。</p><p>析构函数和构造函数调用顺序相反,先构造,后析构。</p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"4d80f935-2ff4-4ab5-a8dd-f75e316a83cb\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[//汽车类\nclass Car{\npublic:\n\tCar(){\n\t\tcout << \"Car 默认构造函数!\" << endl;\n\t\tmName = \"大众汽车\";\n\t}\n\tCar(string name){\n\t\tcout << \"Car 带参数构造函数!\" << endl;\n\t\tmName = name;\n\t}\n\t~Car(){\n\t\tcout << \"Car 析构函数!\" << endl;\n\t}\npublic:\n\tstring mName;\n};\n\n//拖拉机\nclass Tractor{\npublic:\n\tTractor(){\n\t\tcout << \"Tractor 默认构造函数!\" << endl;\n\t\tmName = \"爬土坡专用拖拉机\";\n\t}\n\tTractor(string name){\n\t\tcout << \"Tractor 带参数构造函数!\" << endl;\n\t\tmName = name;\n\t}\n\t~Tractor(){\n\t\tcout << \"Tractor 析构函数!\" << endl;\n\t}\npublic:\n\tstring mName;\n};\n\n//人类\nclass Person{\npublic:\n#if 1\n\t//类mCar不存在合适的构造函数\n\tPerson(string name){\n\t\tmName = name;\n\t}\n#else\n\t//初始化列表可以指定调用构造函数\n\tPerson(string carName, string tracName, string name) : mTractor(tracName), mCar(carName), mName(name){\n\t\tcout << \"Person 构造函数!\" << endl;\n\t}\n#endif\n\t\n\tvoid GoWorkByCar(){\n\t\tcout << mName << \"开着\" << mCar.mName << \"去上班!\" << endl;\n\t}\n\tvoid GoWorkByTractor(){\n\t\tcout << mName << \"开着\" << mTractor.mName << \"去上班!\" << endl;\n\t}\n\t~Person(){\n\t\tcout << \"Person 析构函数!\" << endl;\n\t}\nprivate:\n\tstring mName;\n\tCar mCar;\n\tTractor mTractor;\n};\n\nvoid test(){\n\t//Person person(\"宝马\", \"东风拖拉机\", \"赵四\");\n\tPerson person(\"刘能\");\n\tperson.GoWorkByCar();\n\tperson.GoWorkByTractor();\n}]]></ac:plain-text-body></ac:structured-macro><h3><span>4.3.8 explicit</span>关键字</h3><p>c++提供了<span><a href=\"http://baike.baidu.com/view/390935.htm\">关键字</a>explicit</span>,禁止通过<span><a href=\"http://baike.baidu.com/view/5411414.htm\">构造函数</a></span>进行的隐式转换。声明为<span>explicit</span>的<span><a href=\"http://baike.baidu.com/view/411124.htm\">构造函数</a></span>不能在隐式转换中使用。</p><p><span> </span></p><table class=\"wrapped\"><colgroup><col /></colgroup><tbody><tr><td><p><strong>[explicit</strong><strong>注意<span>]</span></strong></p><p>n explicit用于修饰构造函数,防止隐式转化。</p><p>n 是针对单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造)而言。</p></td></tr></tbody></table><p><br /></p><p><br /></p><p><br /></p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"f1cc5a03-02f6-4d2d-a765-16cee32cab8f\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class MyString{\npublic:\n\texplicit MyString(int n){\n\t\tcout << \"MyString(int n)!\" << endl;\n\t}\n\tMyString(const char* str){\n\t\tcout << \"MyString(const char* str)\" << endl;\n\t}\n};\n\nint main(){\n\n\t//给字符串赋值?还是初始化?\n\t//MyString str1 = 1; \n\tMyString str2(10);\n\n\t//寓意非常明确,给字符串赋值\n\tMyString str3 = \"abcd\";\n\tMyString str4(\"abcd\");\n\n\treturn EXIT_SUCCESS;\n}\n\n]]></ac:plain-text-body></ac:structured-macro><h3><span>4.3.9 </span>动态对象创建</h3><p>当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。</p><p>所以动态的意思意味着不确定性。</p><p>为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然<span>c</span>早就提供了动态内存分配(<span>dynamic memory allocation</span>)<span>,</span>函数<span>malloc</span>和<span>free</span>可以在运行时从堆中分配存储单元。</p><p>然而这些函数在<span>c++</span>中不能很好的运行,因为它不能帮我们完成对象的初始化工作。</p><h4><span>4.3.9.1 </span>对象创建</h4><p>当创建一个<span>c++</span>对象时会发生两件事<span>:</span></p><ol><li>为对象分配内存</li><li>调用构造函数来初始化那块内存</li></ol><p>第一步我们能保证实现,需要我们确保第二步一定能发生。<span>c++</span>强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因。</p><h4><span>4.3.9.2 C</span>动态分配内存方法</h4><p>为了在运行时动态分配内存,<span>c</span>在他的标准库中提供了一些函数<span>,malloc</span>以及它的变种<span>calloc</span>和<span>realloc,</span>释放内存的<span>free,</span>这些函数是有效的、但是原始的,需要程序员理解和小心使用。为了使用<span>c</span>的动态内存分配函数在堆上创建一个类的实例,我们必须这样做<span>:</span></p><p><br /></p><p><br /></p><p><br /></p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"65b64b66-0d7b-44e1-a46e-9cb20c18b530\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n\tPerson(){\n\t\tmAge = 20;\n\t\tpName = (char*)malloc(strlen(\"john\")+1);\n\t\tstrcpy(pName, \"john\");\n\t}\n\tvoid Init(){\n\t\tmAge = 20;\n\t\tpName = (char*)malloc(strlen(\"john\")+1);\n\t\tstrcpy(pName, \"john\");\n\t}\n\tvoid Clean(){\n\t\tif (pName != NULL){\n\t\t\tfree(pName);\n\t\t}\n\t}\npublic:\n\tint mAge;\n\tchar* pName;\n};\nint main(){\n\n\t//分配内存\n\tPerson* person = (Person*)malloc(sizeof(Person));\n\tif(person == NULL){\n\t\treturn 0;\n\t}\n\t//调用初始化函数\n\tperson->Init();\n\t//清理对象\n\tperson->Clean();\n\t//释放person对象\n\tfree(person);\n\n\treturn EXIT_SUCCESS;\n}\n\n]]></ac:plain-text-body></ac:structured-macro><p><strong>问题:</strong></p><table class=\"wrapped\"><colgroup><col /></colgroup><tbody><tr><td><p>1) 程序员必须确定对象的长度。</p><p>2) malloc返回一个<span>void*</span>指针,<span>c++</span>不允许将<span>void*</span>赋值给其他任何指针,必须强转。</p><p>3) malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。</p><p>4) 用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化<span>(</span>构造函数是由编译器调用<span>)</span>,用户有可能忘记调用初始化函数。</p></td></tr></tbody></table><p><br /></p><p>c的动态内存分配函数太复杂,容易令人混淆,是不可接受的,<span>c++</span>中我们推荐使用运算符<span>new </span>和<span> delete.</span></p><h4><span>4.3.9.3 new operator </span></h4><p>C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为<span>new</span>的运算符里。当用<span>new</span>创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。</p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"0e263213-49e3-4ae9-91d8-13936d7f8f91\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[Person* person = new Person;\n相当于:\nPerson* person = (Person*)malloc(sizeof(Person));\n\tif(person == NULL){\n\t\treturn 0;\n\t}\nperson->Init(); 构造函数\n\n]]></ac:plain-text-body></ac:structured-macro><p>New操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。</p><p>现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。</p><p><span> </span></p><h4><span>4.3.9.4 delete operator</span></h4><p>new表达式的反面是<span>delete</span>表达式。<span>delete</span>表达式先调用析构函数,然后释放内存。正如<span>new</span>表达式返回一个指向对象的指针一样,<span>delete</span>需要一个对象的地址。</p><p>delete只适用于由<span>new</span>创建的对象。</p><p>如果使用一个由<span>malloc</span>或者<span>calloc</span>或者<span>realloc</span>创建的对象使用<span>delete,</span>这个行为是未定义的。因为大多数<span>new</span>和<span>delete</span>的实现机制都使用了<span>malloc</span>和<span>free,</span>所以很可能没有调用析构函数就释放了内存。</p><p>如果正在删除的对象的指针是<span>NULL,</span>将不发生任何事,因此建议在删除指针后,立即把指针赋值为<span>NULL</span>,以免对它删除两次,对一些对象删除两次可能会产生某些问题。</p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"ef2ee99e-4283-4eb2-937d-719e25a8b191\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n\tPerson(){\n\t\tcout << \"无参构造函数!\" << endl;\n\t\tpName = (char*)malloc(strlen(\"undefined\") + 1);\n\t\tstrcpy(pName, \"undefined\");\n\t\tmAge = 0;\n\t}\n\tPerson(char* name, int age){\n\t\tcout << \"有参构造函数!\" << endl;\n\t\tpName = (char*)malloc(strlen(name) + 1);\n\t\tstrcpy(pName, name);\n\t\tmAge = age;\n\t}\n\tvoid ShowPerson(){\n\t\tcout << \"Name:\" << pName << \" Age:\" << mAge << endl;\n\t}\n\t~Person(){\n\t\tcout << \"析构函数!\" << endl;\n\t\tif (pName != NULL){\n\t\t\tdelete pName;\n\t\t\tpName = NULL;\n\t\t}\n\t}\npublic:\n\tchar* pName;\n\tint mAge;\n};\n\nvoid test(){\n\tPerson* person1 = new Person;\n\tPerson* person2 = new Person(\"John\",33);\n\n\tperson1->ShowPerson();\n\tperson2->ShowPerson();\n\n\tdelete person1;\n\tdelete person2;\n}\n\n]]></ac:plain-text-body></ac:structured-macro><h4><span>4.3.9.5 </span>用于数组的<span>new</span>和<span>delete</span></h4><p>使用<span>new</span>和<span>delete</span>在堆上创建数组非常容易。</p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"039dfff3-3a43-470e-9e15-459fa3bc6d92\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[//创建字符数组\nchar* pStr = new char[100];\n//创建整型数组\nint* pArr1 = new int[100]; \n//创建整型数组并初始化\nint* pArr2 = new int[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };\n\n//释放数组内存\ndelete[] pStr;\ndelete[] pArr1;\ndelete[] pArr2;]]></ac:plain-text-body></ac:structured-macro><p>当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数。</p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"ddd47e83-54f4-47d5-ae72-c7217faf04a8\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n\tPerson(){\n\t\tpName = (char*)malloc(strlen(\"undefined\") + 1);\n\t\tstrcpy(pName, \"undefined\");\n\t\tmAge = 0;\n\t}\n\tPerson(char* name, int age){\n\t\tpName = (char*)malloc(sizeof(name));\n\t\tstrcpy(pName, name);\n\t\tmAge = age;\n\t}\n\t~Person(){\n\t\tif (pName != NULL){\n\t\t\tdelete pName;\n\t\t}\n\t}\npublic:\n\tchar* pName;\n\tint mAge;\n};\n\nvoid test(){\n\t//栈聚合初始化\n\tPerson person[] = { Person(\"john\", 20), Person(\"Smith\", 22) };\n\tcout << person[1].pName << endl;\n //创建堆上对象数组必须提供构造函数\n\tPerson* workers = new Person[20];\n}\n\n]]></ac:plain-text-body></ac:structured-macro><h4><span>4.3.9.6 delete void*</span>可能会出错</h4><p><strong> </strong>如果对一个<span>void*</span>指针执行<span>delete</span>操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将不执行析构函数<span>.</span>以下代码未调用析构函数,导致可用内存减少。</p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"d32de14b-b240-4ec9-a2ec-4e0c514f022d\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n\tPerson(char* name, int age){\n\t\tpName = (char*)malloc(sizeof(name));\n\t\tstrcpy(pName,name);\n\t\tmAge = age;\n\t}\n\t~Person(){\n\t\tif (pName != NULL){\n\t\t\tdelete pName;\n\t\t}\n\t}\npublic:\n\tchar* pName;\n\tint mAge;\n};\n\nvoid test(){\n\tvoid* person = new Person(\"john\",20);\n\tdelete person;\n}]]></ac:plain-text-body></ac:structured-macro><p><span> </span></p><table class=\"wrapped\"><tbody><tr><td><p><strong>问题:</strong>malloc、<span>free</span>和<span>new</span>、<span>delete</span>可以混搭使用吗?也就是说<span>malloc</span>分配的内存,可以调用<span>delete</span>吗?通过<span>new</span>创建的对象,可以调用<span>free</span>来释放吗?</p></td></tr></tbody></table><p><span> </span></p><h4><span>4.3.9.7 </span>使用<span>new</span>和<span>delete</span>采用相同形式</h4><table class=\"wrapped\"><colgroup><col /></colgroup><tbody><tr><td><p> Person<strong>*</strong> person <strong>=</strong> <strong>new</strong> Person<strong>[</strong>10<strong>];</strong></p><p> <strong>delete</strong> person<strong>;</strong></p></td></tr></tbody></table><p>以上代码有什么问题吗?<span>(vs</span>下直接中断、<span>qt</span>下析构函数调用一次<span>)</span></p><p>使用了<span>new</span>也搭配使用了<span>delete</span>,问题在于<span>Person</span>有<span>10</span>个对象,那么其他<span>9</span>个对象可能没有调用析构函数,也就是说其他<span>9</span>个对象可能删除不完全,因为它们的析构函数没有被调用。</p><p>我们现在清楚使用<span>new</span>的时候发生了两件事<span>: </span>一、分配内存;二、调用构造函数,那么调用<span>delete</span>的时候也有两件事:一、析构函数;二、释放内存。</p><p>那么刚才我们那段代码最大的问题在于:<span>person</span>指针指向的内存中到底有多少个对象,因为这个决定应该有多少个析构函数应该被调用。换句话说,<span>person</span>指针指向的是一个单一的对象还是一个数组对象,由于单一对象和数组对象的内存布局是不同的。更明确的说,数组所用的内存通常还包括“数组大小记录”,使得<span>delete</span>的时候知道应该调用几次析构函数。单一对象的话就没有这个记录。单一对象和数组对象的内存布局可理解为下图<span>:</span></p><p><span><ac:image ac:height=\"189\"><ri:attachment ri:filename=\"image2021-5-8_18-28-24.png\" /></ac:image></span></p><p>本图只是为了说明,编译器不一定如此实现,但是很多编译器是这样做的。</p><p>当我们使用一个<span>delete</span>的时候,我们必须让<span>delete</span>知道指针指向的内存空间中是否存在一个“数组大小记录”的办法就是我们告诉它。当我们使用<span>delete[]</span>,那么<span>delete</span>就知道是一个对象数组,从而清楚应该调用几次析构函数。</p><p><strong>结论<span>:</span></strong></p><table class=\"wrapped\"><colgroup><col /></colgroup><tbody><tr><td><p> 如果在<span>new</span>表达式中使用<span>[]</span>,必须在相应的<span>delete</span>表达式中也使用<span>[].</span>如果在<span>new</span>表达式中不使用<span>[], </span>一定不要在相应的<span>delete</span>表达式中使用<span>[].</span></p></td></tr></tbody></table><h3><span>4.3.10 </span>静态成员</h3><p>在类定义中,它的成员(包括成员变量和成员函数),这些成员可以用关键字<span>static</span>声明为静态的,称为静态成员。</p><p>不管这个类创建了多少个对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象共享。</p><p><span> </span></p><h4><span>4.3.10.1 </span>静态成员变量</h4><p>在一个类中,若将一个成员变量声明为<span>static</span>,这种成员称为静态成员变量。与一般的数据成员不同,无论建立了多少个对象,都只有一个静态数据的拷贝。静态成员变量,属于某个类,所有对象共享。</p><p>静态变量,是在编译阶段就分配空间,对象还没有创建时,就已经分配空间。</p><p><span> </span></p><table class=\"wrapped\"><colgroup><col /></colgroup><tbody><tr><td><ul><li><strong>静态成员变量必须在类中声明,在类外定义</strong><strong>。</strong></li><li><strong>静态数据成员不属于某个对象,在为对象分配空间中不包括静态成员所占空间。</strong></li><li><strong>静态数据成员可以通过类名或者对象名来引用。</strong></li></ul></td></tr></tbody></table><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"118f3f57-9406-4a7f-b190-dad492a87c2b\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n\t//类的静态成员属性\n\tstatic int sNum;\nprivate:\n\tstatic int sOther;\n};\n\n//类外初始化,初始化时不加static\nint Person::sNum = 0;\nint Person::sOther = 0;\nint main(){\n\n\n\t//1. 通过类名直接访问\n\tPerson::sNum = 100;\n\tcout << \"Person::sNum:\" << Person::sNum << endl;\n\n\t//2. 通过对象访问\n\tPerson p1, p2;\n\tp1.sNum = 200;\n\n\tcout << \"p1.sNum:\" << p1.sNum << endl;\n\tcout << \"p2.sNum:\" << p2.sNum << endl;\n\n\t//3. 静态成员也有访问权限,类外不能访问私有成员\n\t//cout << \"Person::sOther:\" << Person::sOther << endl;\n\tPerson p3;\n\t//cout << \"p3.sOther:\" << p3.sOther << endl;\n\n\tsystem(\"pause\");\n\treturn EXIT_SUCCESS;\n}\n\n]]></ac:plain-text-body></ac:structured-macro><h4><span>4.3.10.2 </span>静态成员函数</h4><p>在类定义中,前面有<span>static</span>说明的成员函数称为静态成员函数。静态成员函数使用方式和静态变量一样,同样在对象没有创建前,即可通过类名调用。静态成员函数主要为了访问静态变量,但是,不能访问普通成员变量。</p><p>静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装。</p><p><span> </span></p><ul><li>静态成员函数只能访问静态变量,不能访问普通成员变量</li><li>静态成员函数的使用和静态成员变量一样</li><li>静态成员函数也有访问权限</li><li>普通成员函数可访问静态成员变量、也可以访问非经常成员变量</li></ul><p><br /></p><p><br /></p><p><br /></p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"3383acc2-f7dd-460e-a885-a22e8bbd2d99\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n\t//普通成员函数可以访问static和non-static成员属性\n\tvoid changeParam1(int param){\n\t\tmParam = param;\n\t\tsNum = param;\n\t}\n\t//静态成员函数只能访问static成员属性\n\tstatic void changeParam2(int param){\n\t\t//mParam = param; //无法访问\n\t\tsNum = param;\n\t}\nprivate:\n\tstatic void changeParam3(int param){\n\t\t//mParam = param; //无法访问\n\t\tsNum = param;\n\t}\npublic:\n\tint mParam;\n\tstatic int sNum;\n};\n\n//静态成员属性类外初始化\nint Person::sNum = 0;\n\nint main(){\n\n\t//1. 类名直接调用\n\tPerson::changeParam2(100);\n\n\t//2. 通过对象调用\n\tPerson p;\n\tp.changeParam2(200);\n\n\t//3. 静态成员函数也有访问权限\n\t//Person::changeParam3(100); //类外无法访问私有静态成员函数\n\t//Person p1;\n\t//p1.changeParam3(200);\n\treturn EXIT_SUCCESS;\n}\n\n]]></ac:plain-text-body></ac:structured-macro><h4><span>4.3.10.3 const</span>静态成员属性</h4><p>如果一个类的成员,既要实现共享,又要实现不可改变,那就用<span> static const </span>修饰。<strong>定义静态<span>const</span>数据成员时,最好在类内部初始化</strong>。</p><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"49f2811b-73ba-4cf5-834c-f00f531a32c3\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Person{\npublic:\n\t//static const int mShare = 10;\n\tconst static int mShare = 10; //只读区,不可修改\n};\nint main(){\n\n\tcout << Person::mShare << endl;\n\t//Person::mShare = 20;\n\n\treturn EXIT_SUCCESS;\n}]]></ac:plain-text-body></ac:structured-macro><h4><span>4.3.10.4 </span>静态成员实现单例模式</h4><p>单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。</p><p><ac:image ac:height=\"206\"><ri:attachment ri:filename=\"image2021-5-8_18-31-17.png\" /></ac:image></p><p>Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其默认构造函数和拷贝构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。</p><p><br /></p><table class=\"wrapped\"><colgroup><col /></colgroup><tbody><tr><td><p>用单例模式,模拟公司员工使用打印机场景,打印机可以打印员工要输出的内容,并且可以累积打印机使用次数。</p></td></tr></tbody></table><ac:structured-macro ac:name=\"code\" ac:schema-version=\"1\" ac:macro-id=\"6192f72a-fd87-48b8-892a-99b21d1ad49f\"><ac:parameter ac:name=\"language\">cpp</ac:parameter><ac:plain-text-body><![CDATA[class Printer{\npublic:\n\tstatic Printer* getInstance(){ return pPrinter;}\n\tvoid PrintText(string text){\n\t\tcout << \"打印内容:\" << text << endl;\n\t\tcout << \"已打印次数:\" << mTimes << endl;\n\t\tcout << \"--------------\" << endl;\n\t\tmTimes++;\n\t}\nprivate:\n\tPrinter(){ mTimes = 0; }\n\tPrinter(const Printer&){}\nprivate:\n\tstatic Printer* pPrinter;\n\tint mTimes;\n};\n\nPrinter* Printer::pPrinter = new Printer;\n\nvoid test(){\n\tPrinter* printer = Printer::getInstance();\n\tprinter->PrintText(\"离职报告!\");\n\tprinter->PrintText(\"入职合同!\");\n\tprinter->PrintText(\"提交代码!\");\n}]]></ac:plain-text-body></ac:structured-macro></ac:layout-cell><ac:layout-cell><p><br /></p></ac:layout-cell></ac:layout-section></ac:layout>","representation":"storage","_expandable":{"content":"/rest/api/content/58954451"}},"_expandable":{"editor":"","view":"","export_view":"","styled_view":"","anonymous_export_view":""}},"extensions":{"position":"none"},"_links":{"webui":"/pages/viewpage.action?pageId=58954451","edit":"/pages/resumedraft.action?draftId=58954451","tinyui":"/x/05KDAw","collection":"/rest/api/content","base":"https://wiki.suncaper.net","context":"","self":"https://wiki.suncaper.net/rest/api/content/58954451"},"_expandable":{"container":"/rest/api/space/2CT","metadata":"","operations":"","children":"/rest/api/content/58954451/child","restrictions":"/rest/api/content/58954451/restriction/byOperation","history":"/rest/api/content/58954451/history","ancestors":"","descendants":"/rest/api/content/58954451/descendant","space":"/rest/api/space/2CT"}} |