c结构体和c++类的区别
在c++引入了关键字 类(class) 其和c中的结构体struct用法一样 区别在于c结构体中只能有变量 而在类中除了变量之外还可以有函数和类型
类中的变量叫成员变量 类中的函数叫成员函数 定义在类里面的成员函数默认为inline内联函数
同时在c++中的结构体也升级为了类
在c++中结构体和类一样也可以包含函数了 和类的区别就在于下文的访问限定符上 还有在创建对象的时候不用再加struct了 直接 结构体名 对象名 就创建好结构体对象了 省去了定义结构体时 typede对其进行重命名的操作了
访问限定符
C++中⽤类将对象的属性与⽅法结合在⼀块 让对象更加完善 通过访问权限 选择性的将其接⼝提供给外部
访问限定符有三个 public private protected
public修饰的成员可以在类外被访问 而private和protected只能被类内所访问 在类外无访问权限
private和protected在后续的继承中才能体现出区别
访问权限作用域
从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为止 没有的话就是到类最后的}结束
开始提到的在c++中struct和class的区别在于初始访问权限 struct的初始默认访问权限为public而class的初始访问权限为priivate
类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤::作 ⽤域操作符指明成员属于哪个类域
在class中定义成员变量和函数并不占用空间 class中的成员变量只是声明 没有分配空间 此时就相当于是图纸
在实例化创建对象时候才会分配空间 相当于依靠图纸来造了房子
对象的大小
类实例化出的每个对象都有独立的数据空间 对象的大小包含成员变量 但是不会包含成员函数
因为函数被编译后是⼀段指令,对象中没办法存储,这些指令存储在⼀个单独的区域(代码段),对象中非要存储的话,只能是成员函数的指针 但是没有这个必要 类可以实例化出来多个对象 这些对象的成员变量各不相同但是他们公用成员函数 如果每一个实例化的对象中都包含成员函数更浪费空间
C++规定类实例化的对象也要符合内存对齐的规则 这和c中结构体那块的对其规则一模一样
内存对齐规则
第⼀个成员在与结构体偏移量为0的地址处。
成员变量要对齐到对齐数的整数倍的地址处 对齐数=编译器默认的⼀个对⻬数与该成员大小的较小值 VS中默认的对齐数为8
结构体总大小为:最大对齐数(所有变量类型最⼤者与默认对⻬参数取最小)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体大小 就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
this指针
在上文提到 实例化创建对象的时候 对象只包含成员变量 并不包含成员函数 而是和所有的实例化对象共用类中的成员函数
那么如果有两个实例化对象 a1和a2 此时a1调用成员函数print() 那怎么知道printf()是a1调用的还是a2调用的呢 其实就是用隐含的this指针来解决的
在对象调用成员函数的时候 会隐含的把该对象的地址给传过去
成员函数被调用时候 它的第一个参数其实是 该类类型的地址 叫this this也就是调用对象的地址
用const修饰this指针 使其始终指向调用对象的地址
但是这个过程是隐式进行的 我们不能把这个参数给加上否则就会报错 但是在成员函数中可以使用this指针 对其进行打印做返回值等 成员函数中直接用的成员变量就默认是this指针所对应的对象的不用刻意去写
假设以下程序运行会不会发生什么问题呢
这个程序是可以正常运行的
可能会有疑惑 p不是一个空指针吗 在调用print函数不是进行了解引用吗 那不就是对空指针进行了解引用 不就出问题了吗
前面我们已经提到 在实例化的对象中没有存储成员函数 在对象调用成员函数 不会对其进行解引用再找对应函数这一过程 而是会直接跳转到成员函数的地方进行了调用 也就没有对空指针进行解引用操作
但是下面的程序就会程序崩溃了 因为对空指针进行了解引用的操作
其他都和第一个程序一样 只有在成员函数Print函数中加了打印成员变量_a的操作
其他过程都和第一个程序一样
前面在this指针处提到了 对象在调用成员函数的时候会隐含的把该变量地址传过去(这里传的是p解引用后的地址 也就是p本身) 成员函数也会接收到该地址为this(此时的this为空指针) 那么在打印成员变量的时候就对this进行了解引用 就是对空指针进行了解引用 所有程序就崩了
this指针存在栈区中
类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。
默认成员函数有6个 而重要的就是接下来的四个 构造函数 析构函数 拷贝构造函数 赋值重载
默认成员函数我们要从两个方面来思考
⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
⼆:编译器默认⽣成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
构造函数
构造函数是特殊的成员函数 作用是在创建对象的时候对其进行初始化的操作
构造函数的特点:
1. 函数名与类名相同。
2. ⽆返回值。(返回值啥都不需要给,也不需要写void 就是规定如此)
3. 对象实例化时系统会⾃动调⽤对应的构造函数。
4. 构造函数可以重载
5. 如果我们没有自己写构造函数 则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦我们自己写了一个构造函数就会调用我们自己写的 不会调用自动生成的
6. 我们不写构造时编译器默认⽣成的构造函数及我们自己写的⽆参构造函数全缺省构造函数都叫做默认构造函 数。无参的构造函数和全缺省的构造函数他们构成重载函数 但是在调用的时候会产生歧义 所有我们只能写一个 不是只有我们不写的函数才是默认构造函数不传实参就可以调⽤的构造就叫默认构造。
前4点是构造函数基础 5 6 点有以下总结
我们不写构造函数编译器会自动调用它默认的无参的构造函数 只要我们写一个构造函数 这个默认的就不会调用了 会调用我们自己写的的
那我们写一个带参的构造函数 就没有无参的构造函数了 此时构造函数就必须传参数 我们创建对象时候就必须给它传参数
c++有两种类型 内置类型(基本类型)和⾃定义类型。内置类型就是提供的原⽣数据类型, 如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字自己定义的类型。
7. 我们不写 编译器默认的构造函数对内置类型成员变量是否初始化是不确定的 取决于编译器
而对于自定义类型成员变量,编译器会自动调用这个成员变量的构造函数 这里要求这个自定义类型的成员变量必须有自己的默认构造函数 如果这个成员变量没有默认构造函数,那么就会报错
拿之前的两栈实现队列来举例
在栈中使用第一个默认构造函数或者是编译器默认的无参构造函数 队列创建时候就会自动调用其构造函数识别到其成员变量为自定义类型就会自动调用栈的默认构造函数 可以正常运行
但是如果使用栈的第二个构造函数 队列创建对象时候就会自动调用队列构造函数在识别到变量为自定义类型 自动调用栈的默认构造函数 但是栈使用了第二个构造函数其编译器自动调用的默认构造函数不会调用 也没有其他自己写的默认构造函数 只有传参的 就会出问题
析构函数
析构函数是在对象销毁时完成对资源空间的清理工作 清理的是动态申请的空间
不是完成对对象本⾝的销毁,比如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管
1.析构函数名是在类名前加上字符~。
2. ⽆参数⽆返回值。(这⾥跟构造类似,也不需要加void)
3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会自动调⽤他的析构函数。
6. 还需要注意的是我们自己写析构函数即使这个析构函数没有对自定义类型成员做处理也会自动调用自定义类型对象的析构函数 也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
7. 如果类中没有申请资源时或者默认⽣成的析构就可以⽤析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如但是有资源申请时,⼀定要 ⾃⼰写析构,否则会造成资源泄漏,如Stack。
8. ⼀个局部域的多个对象,C++规定后定义的先析构。
前4点是构造函数基础 由5 6 7 总结
析构函数真正要清理的是动态申请的空间 动态资源 而对于成员变量如果是内置类型不做也不用处理 如果是自定义类型的成员变量会自动调用它的析构函数 所以在成员变量中除了自定义类型之外有动态申请空间的成员变量才需要手动写析构函数
还是拿两栈实现队列举例
在栈中有动态申请空间的数组 所以需要手动写析构函数对数组_a进行手动释放 而在队列中 队列的两个成员变量都是自定义类型的成员变量 不是我们写的析构函数会自动调用自定义类型的栈的析构函数 所以也不需要手动写 而且就算我们在队列中手动写了一个析构函数在里面没有进行什么处理 还是会自动调用自定义类型的成员变量
拷贝构造函数
如果⼀个构造函数的第⼀个参数是自身类 类型的引⽤,如果有额外参数的 额外参数都有默认值,则此构造函数叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。
我们知道 在传值传参及传值返回的时候会创建一个临时对象 这个对象就是实参的拷贝 而在类中这个创建拷贝的过程就是由拷贝构造函数完成的
拷⻉构造的特点:
1. 拷⻉构造函数是构造函数的⼀个重载。
2. 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤ 如果使⽤传值⽅式编译器直接报错因为会引发⽆穷递归调⽤(传值调用这种方式调用构造函数需要传参 而传参又需要调用拷贝构造函数 就会不断循环)。拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引 ⽤,后⾯的参数必须有缺省值。
3. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成 员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构 造。
自己写了拷贝构造函数在内部没有对成员变量进行处理 产生的处理和编译器有关 vs只进行了初始化
写的拷贝构造没有返回值 是自己写的会和编译器隐试生成的拷贝逻辑结合
5. 编译器自动生成的拷贝构造为浅拷贝直接把值给拷贝过去 类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成要求,不需要我们自己写。
成员变量是一个地址指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉会不符合我们的需求,所以需要 我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。
内部是⾃定义类型 Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现 MyQueue的拷⻉构造。
这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就 需要显⽰写拷⻉构造,否则就不需要。
因为如果需要自己写析构就说明该类有除自定义类型之外的动态申请的资源 而这样申请出来的是一个地址 这就是要就行深拷贝自己写的情况了
6.,传值返回和传引用返回都可以 传值返回会产⽣⼀个临时对象调⽤拷⻉构造会多调用一次拷贝构造函数 但是传引用返回如果返回值是个局部变量 就造成了空引用
总结 同样如果类的成员全是内置类型也没有指向什么资源(如 指针)那么 编译器自动生成的拷贝构造函数就可以完成要求 如果有自定义类型的成员 编译器也会自动调用自定义类型成员的拷贝构造函数 也不需要我们处理 只有当成员变量为指针 指向了资源 需要自己写拷贝构造函数
同样拿栈和队列举例
我们先创建了栈st1
在没有自己写的拷贝构造函数 用默认的拷贝构造函数的话就会进行直接的值拷贝 把st1所存的数据直接都给了st2
这样std1和st2中的_a指向的是同一块空间 这样就会出现很多的问题 如: 在最后释放资源的时候就会对同一块空间free两次 就会出问题 还有 俩个栈公用一个空间 一个栈的内容进行了改变 另一个栈也会改变 但是显示给我们的capacity和top只有一个栈会进行改变
创建队列时候 识别到成员变量是自定义类型自动调用成员变量的拷贝构造函数 队列中也会成这样
所有 此时对栈就需要我们自己写拷贝构造函数了
重新申请一块空间进行对应值的拷贝 而不是直接存对应空间的地址 这样拷贝的栈st2和st1就不是同一块的空间了
运算符重载
意义 当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规 定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编 译报错。
• 运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
• 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。
• 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。
• 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
• 不能通过用没有的符号来创建新的操作符:⽐如operator@。
• .* :: sizeof ?: ; .) 这5个运算符不能重载。
• 重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: operator+(int x, int y)
• ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意 义,但是重载operator+就没有意义
赋值运算符重载
赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟 拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。
赋值运算符重载的特点:
1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引⽤,否则会传值传参会有拷⻉
2. 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋 值场景。
3. 没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认拷 ⻉构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义 类型成员变量会调⽤他的赋值重载函数。
4.像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就 可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类,虽然也都是 内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我 们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部 主要是⾃定义类型Stack成员,编译器⾃动⽣成的赋值运算符重载会调⽤Stack的赋值运算符重载, 也不需要我们显⽰实现MyQueue的赋值运算符重载。
赋值运算符和拷贝构造一样 就是也要注意当成员变量有指向资源时要自己手动写
赋值运算符重载用于已经创建好的两个对象的比较 而拷贝构造函数用于一个已有的对象初始化创建一个新对象