【学习笔记】C++(2)

张开发
2026/4/6 3:08:47 15 分钟阅读

分享文章

【学习笔记】C++(2)
C++学习笔记三、进阶 —— 类和对象1、概述2、基础 —— 公有、私有、保护、构造、析构3、拷贝构造、临时对象不能绑定到非const引用问题4、浅拷贝、深拷贝、移动拷贝5、静态6、内联和外联7、链表8、函数模板和类模板9、友元10、继承-派生(1) —— 基础11、继承-派生(2) —— 运算符重载12、继承-派生(3) —— 虚函数和多态13、继承-派生(4) —— 基类和派生类在内存中的布局14、继承-派生(5) —— 虚表和多态的内存布局三、进阶 —— 类和对象1、概述类是面向对象程序设计的基础和核心,也是实现数据抽象的工具。类中的数据具有隐藏性和封装性,类是实现C++许多高级特性的基础2、基础 —— 公有、私有、保护、构造、析构#includeiostream#includestringusingnamespacestd;classStudent{// 类的定义private:// 私有:只能类内访问intid;string secret;// 私有方法仅供内部使用voidprivateInfo(){coutname"'s id is "idendl;}protected:// 保护:类内和派生类可访问string major;public:// 公有:任意访问string name;intage;// 无参构造函数写法1//Student()//{// this-id = 0; this-name = "Unknow";this-age = 18; this-major = "Undecided";this-secret = "default";// cout "无参构造被调用" endl;//}// 无参构造函数写法2Student():id(0),name("Unknown"),age(18),major("Undecided"),secret("default"){cout"无参构造被调用"endl;}// 有参构造函数写法1//Student(int i, string n, int a, string m)//{// this-id = i; this-name = n; this-age = a;this-major = m; this-secret = "init_secret";// cout "有参构造被调用" endl;//}// 有参构造函数写法2Student(inti,string n,inta,string m):id(i),name(n),age(a),major(m),secret("init_secret"){cout"有参构造被调用"endl;}// 析构函数~Student(){cout"析构函数被调用"endl;}// getter/setter 访问私有属性intgetId(){returnid;}// gettervoidsetId(intnewId){id=newId;}// setterstringgetSecret(){returnsecret;}// gettervoidsetSecret(conststrings){secret=s;}// setter// 公有方法可供外部调用voidpublicInfo(){coutname"'s Age:"age" Major:"majorendl;}};intmain(){// 静态分配Student s1;// 无参构造s1.setId(1001);// setters1.name="s1";// 直接访问公有cout"s1's ID: "s1.getId()endl;// getters1.publicInfo();// 还可以用花括号创建实例Student s2={1002,"s2",21,"s222"};Student s3{1003,"s3",20,"s333"};Student arr1[2]={{1004,"s4",22,"s444"},{1005,"s5",21,"s555"}};// 动态分配Student*s6=newStudent(1004,"s6",23,"s666");// 有参构造s6-publicInfo();deletes6;Student*arr2=newStudent[2]{{},{}};return0;// 自动调用析构}TIP1、构造函数特点:必须是共有函数,不能有 return2、一个类默认含有:无参的构造函数、拷贝构造函数;其均可以重载,重载之后将覆盖默认自带的3、调用拷贝构造时,只触发拷贝构造,不会触发无参/有参构造4、重载了有参构造函数或拷贝构造函数后,也需要重载无参构造函数,否则类似于 Student s1; 会报错称类 Student 不存在默认的构造函数5、Student s3(Student()); 这一句可不是拷贝构造,这会被编译器理解为参数为一个返回Student的函数指针的返回值为Student类型的函数s3, 其等价于 Student s3(Student (*)());6、针对于上一点,C++11规定类似于 Student s3{Student()}; 这样的才是构造函数,区别在于将括号换成了花括号7、没标明是 public、private 还是 protect 的话,归为 private8、析构函数的特点:必须是共有函数,不能有 return,不能重载;栈空间下,相同定义域中后创建的先析构,不同定义域中定义域先结束的先析构;堆空间下,回收时立刻析构,不回收则不析构对象数组和对象指针#includeiostream#includestringusingnamespacestd;classStudent{private:string name;public:Student(){name="None";coutname" —— 无参构造"endl;}Student(string name):name(name){coutname" —— 有参构造"endl;}Student(constStudentstu){name=stu.name;coutname" —— 拷贝构造"endl;}~Student(){coutname" —— 析构"endl;}voidsetName(string name){this-name=name;}voiddisplay(){cout"My name is "nameendl;}};intmain(){// 静态分配// 法一// Student stu[3] = { {"A"},{"B"},{"C"} };// 法二:使用临时对象Student stu[3]={Student("A"),Student("B"),Student("C")};// 动态分配// 法一:分配内存时顺带初始化// Student* stu = new Student[3]{ {"A"},{"B"},{"C"} };// 法二// Student* stu = new Student[3];// for (int i = 0; i 3; i++)// {// stu[i].setName(to_string(i));// }// delete[] stu;// 法三// Student* stu = new Student[3];// for (int i = 0; i 3; i++)// {// (stu + i)-setName(to_string(i));// }// delete[] stu;// 法四// Student* stu = new Student[3];// Student* p = stu;// for (int i = 0; i 3; i++)// {// p-setName(to_string(i));// p++;// }// delete[] stu;return0;}TIP1、对于 Student stu[3] = { Student(“A”),Student(“B”),Student(“C”) }; 是不会触发临时对象到对象数组元素的拷贝构造,因为进行了返回值优化(RVO)—— 一种编译器优化技术,它让临时对象直接创建在对象数组元素所在的内存空间上,从而省去了拷贝构造,只会触发临时对象的有参构造函数2、对其规则:长度为n个字节的属性必须放置在n的整数倍的地址上;对象总的内存占用空间是其最长属性长度的整数倍一些正确写法#includeiostream#includestringusingnamespacestd;classStudent{private:string name;public:Student(){name="None";}Student(string name):name(name){}voiddisplay(){coutnameendl;}};intmain(){// 静态分配的写法Student s1;// 正确写法 触发无参构造// Student s2(); // 错误写法 这会被视为一个函数Students3_1("s3_1");// 正确写法 触发有参构造Student s3_2{"s3_2"};// 正确写法 触发有参构造Student s4={"s4"};// 正确写法 触发有参构造Student arr1[2]{{"s5"},{"s6"}};// 正确写法Student arr2[2]={{"s7"},{"s8"}};// 正确写法// 动态分配Student*s9_1=newStudent;// 正确写法,触发无参构造Student*s9_2=newStudent();// 正确写法,触发无参构造Student*s10=newStudent{};// 正确写法,触发无参构造Student*s11=newStudent("s11");// 正确写法,触发有参构造Student*s12=newStudent{"s12"};// 正确写法,触发有参构造Student*arr3=newStudent[2]{{},{"s13"}};// 正确写法return0;}TIP:静态分配且采用括号时不能触发无参构造,只能触发有参构造。如上不能 Student s2(); 但可以 Student s3_1( “s3_1” );3、拷贝构造、临时对象不能绑定到非const引用问题拷贝方式// 创建一个对象Student s1;// 使用已有的实例进行拷贝Students2(s1);Student s3=s1;Student s4{s1};// 使用临时对象进行拷贝Student s3=Student();Student s4=Student(s1);Student s4{Student()};TIP1、拷贝构造的参数必须加上,而const可加可不加(建议,但非必须)2、定义一个对象顺便让它等于一个同类对象,这触发的是拷贝构造,而不是赋值,因为定义的时候它还没被构造出来,需要先进行构造,而不能直接进行赋值;如果是 Student s; s=xxx; 那么这是先触发Student的无参构造再接上一个赋值语句必须加上 的原因:避免无限递归。根本原因在于如果不加上,则函数在执行时,会复制一份参数(不论是基本数据类型、指针,还是对象等)的副本,而这种复制会触发拷贝构造,进而陷入无限循环// 错误示例:没有Student(Student other){id=other.id;}Student s1;// 已有对象Students2(s1);// 调用拷贝构造函数// 陷入无限循环的过程// 第1步:调用 Student s2(s1),触发了s1的拷贝构造函数// ↓// 第2步:需要创建参数 other(值传递!)// ↓// 第3步:创建 other 需要拷贝 s1 → 调用s1拷贝构造函数// ↓// 第4步:再次需要创建参数 other(值传递!)// ↓// 第5步:创建 other 需要拷贝 s1 → 调用s1拷贝构造函数// ↓// 第6步:再次需要创建参数 other...// ↓// 无限循环:无限调用s1的拷贝构造函数!TIP:引用 的意思是 “直接使用原对象,不创建副本”建议加上 const 的原因:安全,防止引用的数据被修改// 不加const:允许但危险Student(Studentother){id=other.id;other.id=0;// 可以修改原对象!这不应该发生}// 加const:安全,推荐Student(constStudentother){id=other.id;// other.id = 0; // 编译错误!不能修改}建议加上 const 的原因还有:有助于提高编程的灵活性,因为 C++ 规定 ”临时对象不能绑定到非const引用“首先解释 ”临时对象” 和 “绑定到引用”:// Q:什么是临时对象// A:下面这些创建的都是临时对象(用完就扔)Student()// 默认构造临时对象Student(100)// 有参构造临时对象createStudent()// 函数返回的临时对象Student{}// C++11统一初始化临时对象// Q:什么是绑定到引用// A:实例s是普通对象,可以绑定到 Studentvoidfunc(Studentref)// ref绑定到传入的对象{ref.name="修改";}Student s;func(s);那么为什么临时对象不能绑定到非const引用?这并不是技术限制,而是 C++ 故意这么禁止的,因为认为参数不加 const 的话,那么有可能对临时对象进行修改,而随着函数的执行完成,被修改的临时对象也随即被销毁,这会让可能存在的修改临时对象的行为显得没有意义,因此故意这么禁止// 演示不给构造函数添加 const 时会引发的错误#includeiostream#includestringusingnamespacestd;classStudent{public:Student(){}Student(Studentother)// 没有const{cout"拷贝构造"endl;}};StudentcreateStudent(){Student s;returns;// 返回的是临时对象}intmain(){// 创建一个对象Student s1;// 情况1:明确创建临时对象Student s2=Student(s1);// 出错:临时对象不能绑定到非const引用// 情况2:显式调用构造函数Student s3=Student();// 出错:临时对象// 情况3:用{}初始化(C++11)Student s4{Student()};// 错误:临时对象// 情况4:函数返回(某些编译器设置下)autocreate=[]()-Student{Student s;returns;// 可能出错};Student s5=create();// 可能出错}TIP1、解决方法:只需为构造函数的参数加上 const 即可2、总结:临时对象可以传递给普通的形参例如参数是 Student other,但是如果形参是一个引用的话:Student other,那么为了临时对象能传递进去则必须给形参再加上一个 const 成为 const Student other写法是否允许建议原因Student(Student other)×禁止无限递归Student(Student other)√不建议不能绑定临时对象Student(const Student other)√推荐安全、灵活4、浅拷贝、深拷贝、移动拷贝浅拷贝:只复制指针,不复制指向的数据;多个对象共享同一块内存;危险在于一个对象删除内存会影响其他对象classShallowArray{private:int*data;intsize;public:// 默认拷贝构造(浅拷贝) —— 对于指针来说,只进行复制ShallowArray(){}ShallowArray(constShallowArrayother):data(other.data),size(other.size){cout"浅拷贝构造"endl;}};深拷贝:每个对象有独立的内存;优点在于安全,缺点在于性能开销大classDeepArray{private:int*data;intsize;public:// 深拷贝构造DeepArray(){}DeepArray(constDeepArrayother){size=other.size;data=newint[size];// 申请新内存for(inti=0;isize;i++){data[i]=other.data[i];// 复制数据}cout"深拷贝构造"endl;}~DeepArray(){delete[]data;// 安全,不冲突}};移动拷贝:原对象变为"空壳";优点在于高性能,适合临时对象classMoveArray{private:int*data;intsize;

更多文章