文章目录
2. 虚函数vs纯虚函数
- virtual修饰的成员函数就是虚函数,它允许在子类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和子类的同名函数。引入虚函数是为了动态绑定。
- 纯虚函数除了关键字virtual外,还要 =0, 它是在基类中声明但没有定义的虚函数,要求子类必须提供实现。引入纯虚函数是为了派生接口,即子类仅仅只是继承函数的接口。
3. 重写vs重载vs隐藏
- 重写:发生继承关系中,子类重写父类的方法。
- 重载:发生在同一个类中,函数名相同,但参数个数或类型不同。
- 隐藏:子类函数屏蔽了与其同名的基类函数,有以下两种情况:
1、参数不同,基类函数被隐藏 (而不是重载)。
2、参数相同,但基类函数没有virtual关键字,基类函数被隐藏 (而不是重写)。
3.1. 为什么C++可以重载?
- C++引入了命名空间,以及作用域,比如类作用域,命名空间作用域。
- 函数在编译期间,链接符号的时候,会在符号后追加一些特殊标识,比如add函数,变成add@123。
4. struct vs union
- 相同:都可以将不同类型的变量组合成一个整体。
- 区别
- struct 里的每个成员都有自己独立的内存空间;sizeof(struct) 是内存对齐后所有成员长度的总和。
- union 里的所有成员共享同一内存空间;sizeof(union) 是内存对齐后最大的数据成员长度。
4.1. 为什么要内存对齐?
- 确保代码在不同平台上的兼容性。
- 提高内存访问的效率。
- ps:在Visual Studio中,默认对齐值8,将它与结构体中最大的数据成员长度进行比较,取两者的较小值,设定为实际对齐值。
5. static作用
- static修饰的成员变量就是类变量,是类的所有对象共有的。
- static修饰的成员函数就是类方法,类方法不能访问对象变量只能访问类变量,可以由类名直接调用,也可以由对象调用。
- static修饰的局部变量就是局部静态变量,在函数内可以访问到。
- static修饰的全局变量就是全局静态变量,是当前文件内可以访问到。
- ps:对象变量,是每个对象单独拥有的。
- ps:const强调值不能被修改,而static强调内存只有一份拷贝。
6. 空类vs空结构体
- 空类:默认private。
- 空结构体:默认public。
6.1. 八个默认函数:
- 构造函数 【A();】
- 析构函数 【~A();】
- 拷贝构造函数 【A (const A&);】
- 重载赋值运算符 【A&operator = (const A&);】
- 重载取址运算符 【A* operator& ();】
- 重载取址运算符const 【const A* operator& () const;】
- 移动构造函数(C++11) 【A(A&&);】
- 重载移动赋值运算符(C++11)【A& operator = (const A&&);】
6.2. 为什么空类占用1字节
- 因为如果对象完全不占用内存空间,空类就无法取得实例的地址,this指针失效,因此不能被实例化。而类的定义是由数据成员和成员函数组成的,在没有数据成员情况下,还可以有成员函数,因此仍然需要实例化。
7. const作用
- 限定变量不可修改。
- 限定成员函数不可修改数据成员(后置const)。
- 成员函数的返回值类型是const,则返回值不是左值(前置const)。
- 用const对函数的参数修饰,表面是输入参数,在函数内不可写。
- const函数只能调用const函数,非const函数可以调用const函数。
7.1 指针常量vs常量指针vs常量指针常量
- 指针常量,即指针本身是常量,所以指针的值(内存地址)不能改变,示例如下。
int a = 10, b = 20;
int* const p= &a;
p = &b; //错误,指针存放的内存地址不可变
*p= 100; //正确,内存地址存放的内容可以改变
- 常量指针,即指向常量的指针,不能通过指针修改指向的内容,示例如下。
const int a = 10;
int b = 20;
const int* p = &a;
p = &b; //正确, 指针存放的内存地址可变
*p = 100; //错误,指针指向的内容不可变
b = 100; //正确,可以通过原来的声明修改
- 常量指针常量,即指向常量的指针本身也是常量,不能通过指针修改指向的值,指针的值不能改变,示例如下。
const int a = 10;
int b = 20;
const int* const p = &a;
p = &b; //错误, 指针存放的内存地址不可变
*p = 100; //错误,指针指向的内容不可变
8. 接口vs抽象类
- 抽象类:带有纯虚函数的类。
抽象类作用:为了扩展和重用。 - 接口:没有数据成员;成员函数都是公有的、都是纯虚函数,虚析构函数除外;是完全抽象的类。
接口作用:只提供了一种规范,实现接口的类必须实现接口中的所有方法。 - 相同:都不能实例化,但可以创建指针。
- 代码如下。
// ConsoleApplication5.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include<iostream>
using namespace std;
//抽象类
class Shape
{
protected:
//数据成员:价格和面积
double price;
double area;
public:
//构造函数
Shape() :price(100),area(0) {}
//虚析构函数
virtual ~Shape() { printf("%s\n", "Delete shape"); }
//纯虚函数:获取图形描述和获取价格
virtual void getDescription() = 0;
virtual void getPrice() = 0;
};
//接口
class Draw
{
public:
//虚析构函数
virtual ~Draw() { printf("%s\n", "Delete Draw"); }
//纯虚函数:输出图形周长
virtual void drawLen() = 0;
};
//具体类
class Circle : public Shape,public Draw
{
private:
double radius;
public:
Circle(double r) : radius(r) { area = 3.14 * radius * radius; price = 100 + area * 6; }
~Circle() { printf("%s%f\n", "Delete circle with radius ",radius); }
void getDescription() { printf("%s%f\n", "Circle with radius ",radius);}
void getPrice(){ printf("%s%f%s%f\n", "Circle with area ", area," price ",price); }
void drawLen() { printf("%s%f\n", "Circle with len ", 2 * 3.14 * radius); }
};
int main() {
Circle c(5.0);
Shape* s = &c; //基类(Shape)指针指向子类(Circle)对象
s->getDescription();
s->getPrice();
Draw* d = &c; //基类(Draw)指针指向子类(Circle)对象
d->drawLen();
_CrtDumpMemoryLeaks();
return 0;
}
- 程序执行结果,如下图。
9. 浅拷贝vs深拷贝
- 浅拷贝只是将指针拷贝,指向同一块内存。
- 深拷贝是直接将内存拷贝一份。
9.1. 深拷贝应用场景
- 【注意】 当类成员变量是指针,为它动态分配内存时,有以下两种bug情况:
1、若使用默认的重载赋值运算符进行浅拷贝,即a和b指向同一内存,但b曾指向的内存不会被删除,造成内存泄漏;若一方离开了它的生存空间,使用析构函数释放资源,另一方会变成悬空指针,导致未定义行为;同时当另一方调用析构函数时,会因重复释放同一堆空间而触发中断。
2、若使用默认的拷贝构造函数进行浅拷贝,会重复释放同一内存。
所以,为避免这两种bug情况,需要进行深拷贝。 - 修改一个对象不会影响到另一个对象时,进行深拷贝以确保每个对象都有自己独立的数据副本。
9.2. 调用拷贝构造函数的三种情况
- 用一个对象去初始化另一个对象时。
- 当函数的参数是对象时,形参是实参的副本,即拷贝构造了一个新对象。
- vs 但当函数的参数是对象引用或对象指针时,是直接传递对象this指针,无需拷贝。
- 当函数的返回值是对象引用时,由于临时对象离开了函数就会消失,所以要对临时对象进行拷贝,构造一个新对象。【注意】 当临时对象出了函数作用域后,存放它数据的那块内存随时会被占用,写入新数据,所以在拷贝构造过程中 【存在安全隐患】,即它可能拷贝别的内容,代码和反汇编如下。
- vs 但当函数的返回值是对象指针时,由于指针指向临时对象,所以临时对象还没消失,并且也无需进行拷贝。
- vs 【注意】 但当函数的返回值是对象时,不一定会调用拷贝构造函数,因为C++编译器通常会进行返回值优化,即它直接传递保存返回值的对象this指针,然后在函数内直接构造,由于这个对象是在函数外定义的,所以不会受函数作用域的影响。
#include <iostream>
using namespace std;
class A {
private:
int data;
public:
A(int i) { data = i; }
A(const A& a)
{
data = a.data;
cout << "拷贝构造函数执行完毕" << endl;
}
};
A& getA() {
A a(3);
return a;
}
int main() {
A d1 = getA();
return 0;
}
eax保存返回的临时对象地址,如下图。
由于临时对象的内存空间被占用了,写入了新数据,所以新对象拷贝了错误的内容,如下图。
9.3. 写时拷贝
- 在使用系统重要的dll或者系统一些函数的时候,系统为了节省空间和提高性能,会直接映射一份共享地址,但当我们对其进行修改时,会触发写时拷贝,会拷贝一份给我们进程内使用,防止我们去修改共享的地址,影响整个系统。
10. 友元
- 通过友元,普通函数或另一个类的成员函数可以访问到类的私有成员和保护成员。
- 友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。
10.1. 友元函数
- friend修饰的函数就是友元函数,可以访问类中的私有成员和保护成员,只需要在类中声明。
- 如普通函数声明友元,可以修改类的数据成员,代码如下。
class MyClass {
private:
int privateMember;
protected:
double protectedMember;
public:
MyClass(int i, double d) : privateMember(i), protectedMember(d) {}
friend void friendFunction(MyClass& obj);// 声明友元函数
void showData(){
cout << privateMember << " " << protectedMember << endl;
}
};
// 定义友元函数
void friendFunction(MyClass& obj) {
obj.privateMember = 44;
obj.protectedMember = 55;
}
int main() {
MyClass obj(10,42);
friendFunction(obj);
obj.showData(); //输出44 55
return 0;
}
10.2. 友元类
- friend修饰的类就是友元类,它所有成员函数都是另一个类的友元函数,即都可以访问另一个类的私有成员和保护成员。
- 如类声明友元,都可以访问另一个类的数据成员,代码如下。
class MyClass {
private:
int privateMember;
protected:
double protectedMember;
public:
MyClass(int i, double d) : privateMember(i), protectedMember(d) {}
friend class FriendClass; // 声明友元类
void showData(){
cout << privateMember << " " << protectedMember << endl;
}
};
//定义友元类
class FriendClass
{
private:
int value;
public:
FriendClass() :value(0) {}
void setValue(MyClass& obj){
value = obj.privateMember + obj.protectedMember;
}
void showValue() {
cout << value << endl;
}
};
int main() {
MyClass obj(10,42);
FriendClass objf;
objf.setValue(obj);
objf.showValue(); //输出52
return 0;
}