最近更新于 2024-05-05 14:18
环境
Debian 11 (arm64)
编译器 g++ 10.2.1;编译标准 C++20;参数:-std=c++20 -no-pie -Wall -Werror=return-type -Werror=non-virtual-dtor -Werror=address -Werror=sequence-point -Werror=format-security -Wextra -pedantic -Wimplicit-fallthrough -Wsequence-point -Wswitch-unreachable -Wswitch-enum -Wstringop-truncation -Wbool-compare -Wtautological-compare -Wfloat-equal -Wshadow=global -Wpointer-arith -Wpointer-compare -Wcast-align -Wcast-qual -Wwrite-strings -Wdangling-else -Wlogical-op -Wconversion -g -O0
类
class 和 struct
在 C 语言中并没有 class 这个关键词,但是有 struct 结构体。C 中也是存在面向对象设计思想的,而实现载体就是 struct。
比如,C 语言中的 FILE 文件指针,通过 struct 封装了文件都具有的一些属性,操作文件的打开、读、写、关闭方法都封装了对应的函数。不是说一定要显式的搞个什么类才叫面向对象,更重要的是这种思想。
C++ 中有了 class,而 struct 也是 C++ 中的类,只是它们的默认属性不同,class 默认私有,struct 默认公有。
构造函数和析构函数
#include <iostream>
class Person
{
private: // 私有,仅内部成员可以访问
std::string m_name;
public:
/**
* @brief 默认构造函数,
* 在实例化对象时自动执行,
* 一般用于默认初始化,
* 如果没有定义,编译器也会自动添加一个
*/
Person();
/**
* @brief 拷贝构造函数,
* 该函数实现将一个对象的属性值复制给另外一个同类对象,
* 如果没有定义,编译器会自动添加一个,默认一一对应拷贝变量值
* @param person 拷贝对象
*/
Person(const Person &person);
/**
* @brief 设置名字
* @param name 名字
*/
void setName(std::string name);
/**
* @brief 获取名字
* @return 名字
*/
std::string getName();
/**
* @brief 析构函数,
* 在实例对象被销毁时自动执行,
* 一般用于清理善后,
* 如果没有定义,编译器也会自动添加一个
*/
~Person();
};
Person::Person()
{
m_name = "空";
std::cout << "默认构造函数" << std::endl;
}
Person::Person(const Person &person)
{
m_name = person.m_name;
std::cout << "拷贝构造函数" << std::endl;
}
void Person::setName(std::string name)
{
m_name = name;
}
std::string Person::getName()
{
return m_name;
}
Person::~Person()
{
std::cout << "析构函数" << std::endl;
}
int main()
{
Person p1; // 默认构造
p1.setName("小明");
Person p2(p1); // 拷贝构造
std::cout << p2.getName() << std::endl;
}
拷贝构造函数
上一个例子也定义了拷贝构造函数,即使不定义,编译器也会自动添加。(编译器自动添加:构造函数、拷贝构造函数、析构函数和赋值重载)
编译器添加的拷贝构造函数是浅拷贝,如果类成员变量不是存储在堆区还好,如果是的话,使用浅拷贝时就相当于复制地址,等同于另起了一个别名指向同一块内存,在析构释放内存的时候就有可能出现重复 delete 的问题。所以需要自己写深拷贝构造函数实现,拷贝时新申请一块内存地址,再赋值进去。
#include <iostream>
class MyClass
{
private:
int *m_num = NULL;
public:
MyClass()
{
m_num = new int(0);
}
MyClass(int num)
{
if (m_num != NULL)
{
delete m_num;
m_num = NULL;
}
m_num = new int(num);
}
int get()
{
return *m_num;
}
MyClass(MyClass &mc)
{
m_num = new int(mc.get());
}
~MyClass()
{
if (m_num != NULL)
{
delete m_num;
m_num = NULL;
}
}
};
int main()
{
MyClass mc1(10);
MyClass mc2(mc1);
MyClass mc3 = mc1;
std::cout << mc2.get() << " " << mc3.get() << std::endl;
}
当我注释掉自定义的拷贝构造函数以后,编译运行时就会出现错误,错误的位置就在 delete
匿名对象
通过 “类名()” 方式使用,而不实例化对象名,这种方式使用调用完当前语句空间就会被释放,即为匿名对象。
#include <iostream>
class MyClass
{
public:
void print(std::string s)
{
std::cout << s << std::endl;
}
};
int main()
{
MyClass().print("Hello world!"); // 匿名对象
MyClass mc;
mc.print("你好,世界!");
}
static 成员
静态成员变量
静态成员变量不管有多少个对象,实际在内存中只有一份,在编译的时候就给分配好了内存空间。即使没有实例化的对象(如果是公开的成员的话),可以直接通过 类型::变量名 进行使用,如果有实例的对象的话,也可以通过 对象名::变量名 使用,它们实际都是对同一个变量进行操作,即共用一个变量。
#include <iostream>
class ClassName
{
private:
static int m_num1;
public:
static int m_num2;
void setNum(int num);
int getNum();
};
// 静态变量在类外初始化
int ClassName::m_num1 = 0;
int ClassName::m_num2 = 0;
void ClassName::setNum(int num) // 写私有成员
{
m_num1 = num;
}
int ClassName::getNum() // 读私有成员
{
return m_num1;
}
int main()
{
ClassName c1;
c1.setNum(77);
c1.m_num2 = 99;
ClassName c2;
std::cout << "c2.m_num1:" << c2.getNum() << "\nc2.m_num2:" << c2.m_num2 << "\nClassName::m_num2:" << ClassName::m_num2 << std::endl;
}
上面的例子,我实例化了一个 c1,并通过 c1 为静态成员变量设置了值。然后又实例化了一个对象 c2,通过 c2 获取静态成员变量的值就是前面通过 c1 设置的。使用类名也可以直接读取前面设置的公开静态成员变量。
下面又一个例子,深入分析静态成员变量
#include <iostream>
class C1
{
static int num1;
int num2;
};
class C2
{
int num1;
int num2;
};
int main()
{
C1 c1;
C2 c2;
std::cout << "int " << sizeof(int) << std::endl;
std::cout << "C1 " << sizeof(C1) << " c1 " << sizeof(c1) << std::endl;
std::cout << "C2 " << sizeof(C2) << " c2 " << sizeof(c2) << std::endl;
}
int 4 字节,C1 类中一个静态变量,一个普通变量,总大小为 4 字节 ,与之对比的 C2 类中是两个普通变量,总大小为 8 字节。
则说明静态变量实际并不在类中,是独立于类开辟的储存空间。应该是和普通静态变量一样,都是存放在全局数据区,和全局变量一样,编译的时候就已经分配好了内存空间,而不是运行到定义的地方才分配。所以即使没有实例化对象也能对它进行操作,只是被挂到类上进行使用,对它的操作很多时候就和非成员变量差不多。
静态成员函数
静态成员函数不能访问类中的变量,可以通过类名调用,也可以通过实例对象调用。
#include <iostream>
class ClassName
{
public:
static void fun1();
};
void ClassName::fun1()
{
std::cout << "静态成员函数" << std::endl;
}
int main()
{
ClassName c;
c.fun1();
ClassName::fun1();
}
const 成员
常成员变量
使用 const 修饰的变量只能初始化一次,不能修改值。若要对常成员变量初始化为指定值,就只能通过构造函数参数初始化列表进行
#include <iostream>
class ClassName
{
private:
const int m_num1;
public:
const int m_num2;
ClassName(int num1, int num2);
int getNum1();
};
ClassName::ClassName(int num1, int num2) : m_num1(num1), m_num2(num2)
{
;
}
int ClassName::getNum1()
{
return m_num1;
}
int main()
{
ClassName c(1, 2);
std::cout << "m_num1=" << c.getNum1() << "\n" << "m_num2=" << c.m_num2 << std::endl;
}
常成员函数
常成员函数内部可以读成员变量值,但是无法向成员变量写入,即成员变量在常成员函数内部具有只读属性。
#include <iostream>
class ClassName
{
private:
int m_num;
public:
void fun() const;
};
void ClassName::fun() const
{
m_num = 1;
}
int main()
{
}
友元
友元函数
在类中使用 friend 声明一个非成员函数后,该函数就可以访问该类的私有成员了
#include <iostream>
class ClassName
{
private:
int m_num;
friend int getNum(ClassName c);
public:
ClassName();
};
ClassName::ClassName()
{
m_num = 6;
}
int getNum(ClassName c)
{
return c.m_num;
}
int main()
{
ClassName c;
std::cout << getNum(c) << std::endl;
}
友元类
在一个类中声明另外一个类为友元,则被声明的类中可以使用声明者的私有成员
#include <iostream>
class A
{
private:
int numA;
public:
friend class B;
A();
};
A::A()
{
numA = 1;
}
class B
{
private:
int numB;
public:
friend class C;
B();
int get(A a);
};
B::B()
{
numB = 2;
}
int B::get(A a)
{
return a.numA;
}
class C
{
public:
int get(B b);
};
int C::get(B b)
{
return b.numB;
}
int main()
{
A a;
B b;
C c;
std::cout << b.get(a) << std::endl;
std::cout << c.get(b) << std::endl;
}
这里有三个类,分别为 A、B 和 C。A 中有私有变量 numA 初始化为 1,B 中有私有变量 numB 初始化为 2。A 类中声明 B 为友元类,B 类中声明 C 为友元类。则 C 类可以使用 B 类的私有成员,B 类可以使用 A 类的私有成员。
继承与派生
一个类继承另外一个类后,就可以一定程度上使用它的成员。一般用于扩充功能,父类实现了一些功能,子类继承父类继续添加新功能。
两者的称呼一般有两套
若 B 从 A 继承
A | B |
---|---|
父类 | 子类 |
基类 | 派生类 |
权限
类的权限有三种:
- public 公有:类内、类外和子类都可访问
- private 私有:仅类内可以访问
- protected 保护:类内和子类可以访问
继承后的权限
父类权限 \ 继承权限 | public | private | protected |
---|---|---|---|
public | public | private | protected |
private | 不可访问 | 不可访问 | 不可访问 |
protected | protected | private | protected |
权限严格程度:private > protected > public,哪个严格,继承后就是哪种权限,父类为私有的就完全不能继承给子类。
单继承
一个子类只继承一个父类。这里写一个多级继承的例子,B 从 A 继承,C 从 B 继承,即 A->B->C
#include <iostream>
class A
{
public:
A()
{
std::cout << "A 构造函数" << std::endl;
}
~A()
{
std::cout << "A 析构函数" << std::endl;
}
};
class B : public A // 从 A 继承
{
public:
B()
{
std::cout << "B 构造函数" << std::endl;
}
~B()
{
std::cout << "B 析构函数" << std::endl;
}
};
class C : public B // 从 B 继承
{
public:
C()
{
std::cout << "C 构造函数" << std::endl;
}
~C()
{
std::cout << "C 析构函数" << std::endl;
}
};
int main()
{
C c;
}
多继承
一个子类继承多个父类。这里写一个例子 C 从 A 和 B 继承
#include <iostream>
class A
{
public:
A()
{
std::cout << "A 构造函数" << std::endl;
}
~A()
{
std::cout << "A 析构函数" << std::endl;
}
};
class B
{
public:
B()
{
std::cout << "B 构造函数" << std::endl;
}
~B()
{
std::cout << "B 析构函数" << std::endl;
}
};
class C : public A, public B // 从 A 和 B 继承
{
public:
C()
{
std::cout << "C 构造函数" << std::endl;
}
~C()
{
std::cout << "C 析构函数" << std::endl;
}
};
int main()
{
C c;
}
子类父类同名变量
父类变量和子类同名时,默认使用子类的,访问父类的需要指定作用域
#include <iostream>
class A
{
public:
int num;
int getA()
{
return num;
}
};
class B
{
public:
int num;
int getB()
{
return num;
}
};
class C : public A, public B
{
public:
int num;
int getC()
{
return num;
}
};
int main()
{
C c;
c.num = 10;
c.A::num = 9;
c.B::num = 8;
std::cout << c.getA() << " " << c.getB() << " " << c.getC() << std::endl;
}
菱形继承
如图中,A 有一个变量 n,分别继承给 A1 和 A2,AA 又从 A1 和 A2 继承,则 AA 中会分别有来自 A1 和 A2 的两个 n。但是实际只需要使用一个 n,这样重复就会浪费内存资源。
这里用代码复现一下上图的菱形继承
#include <iostream>
class A
{
public:
int n;
};
class A1 : public A
{
};
class A2 : public A
{
};
class AA : public A1, public A2
{
};
int main()
{
AA aa;
aa.A1::n = 1;
aa.A2::n = 2;
std::cout << aa.A1::n << " " << aa.A2::n << std::endl;
}
虚继承
而通过虚继承就可以解决这个问题,在 A1 从 A 继承和 A2 从 A 继承时都使用 virtual 修饰,那么最终继承到 AA 时,最终只会分配一个 n。
#include <iostream>
class A
{
public:
int n;
};
class A1 : virtual public A
{
};
class A2 : virtual public A
{
};
class AA : public A1, public A2
{
};
int main()
{
AA aa;
aa.A1::n = 1;
std::cout << aa.n << std::endl;
aa.A2::n = 2;
std::cout << aa.n << std::endl;
}
多态
多态分为静态多态和动态多态。静态多态在编译时就确定了函数地址,比如函数重载和运算符重载,复用名字,但是编译时就确定了要调用的是谁;而动态多态,则是在运行时才确定函数地址,比如派生类和虚函数实现的运行时多态。
#include <iostream>
class Person
{
public:
void speak()
{
std::cout << "说话" << std::endl;
}
};
class Xiaoming : public Person
{
public:
void speak()
{
std::cout << "小明说话" << std::endl;
}
};
class Xiaohong : public Person
{
public:
void speak()
{
std::cout << "小红说话" << std::endl;
}
};
int main()
{
Xiaoming xm;
Xiaohong xh;
Person &p1 = xm;
Person &p2 = xh;
p1.speak();
p2.speak();
}
尽管指向的是两个子类,但是实际执行的还是父类的函数,这是因为编译的时候确定了调用函数的地址。
#include <iostream>
class Person
{
public:
virtual void speak()
{
std::cout << "说话" << std::endl;
}
virtual ~Person()
{
}
};
class Xiaoming : public Person
{
public:
void speak()
{
std::cout << "小明说话" << std::endl;
}
};
class Xiaohong : public Person
{
public:
void speak()
{
std::cout << "小红说话" << std::endl;
}
};
int main()
{
Xiaoming xm;
Xiaohong xh;
Person &p1 = xm;
Person &p2 = xh;
p1.speak();
p2.speak();
}
通过 virtual 修饰函数后(析构函数也要),则做到动态多态,运行时确定指向的函数地址。
纯虚函数(及纯虚析构)
前面写的例子的时候,父类的函数也给了定义,但是实际我们通常希望的是通过子类来实现具体的功能,父类不需要实现,此时就可以用到纯虚函数,直接在后面写上 “= 0” 就行。
另外存在纯虚函数的类为抽象类,无法实例化对象。
#include <iostream>
class Person
{
public:
virtual void speak() = 0; // 纯虚函数
virtual ~Person() = 0; // 纯虚析构
};
Person::~Person() {} // 纯虚析构也必须要定义函数实体
class Xiaoming : public Person
{
private:
std::string *s;
public:
Xiaoming()
{
s = new std::string("小明");
std::cout << "小明构造" << std::endl;
}
void speak()
{
std::cout << *s << std::endl;
}
~Xiaoming()
{
delete s;
std::cout << "小明析构" << std::endl;
}
};
class Xiaohong : public Person
{
private:
std::string *s;
public:
Xiaohong()
{
s = new std::string("小红");
std::cout << "小红构造" << std::endl;
}
void speak()
{
std::cout << *s << std::endl;
}
~Xiaohong()
{
delete s;
std::cout << "小红析构" << std::endl;
}
};
int main()
{
Person *p = new Xiaoming;
p->speak();
delete p;
p = new Xiaohong;
p->speak();
delete p;
}
运算符重载
下面只举部分例子用作参考
+ 号重载
这里写了一个例子,类中有两个数,通过对两个实例对象相加,实现两个对象的两个对应数字相加。C++ 中原生的 + 号并不能直接满足这里的要求,就需要自行重写一个 + 号的功能满足这里的条件。
成员函数实现
#include <iostream>
class MyClass
{
private:
int m_num1;
int m_num2;
public:
MyClass()
{
m_num1 = 0;
m_num2 = 0;
}
void set1(int num)
{
m_num1 = num;
}
void set2(int num)
{
m_num2 = num;
}
int get1()
{
return m_num1;
}
int get2()
{
return m_num2;
}
MyClass operator+(MyClass &mc)
{
MyClass tmp;
tmp.set1(m_num1 + mc.get1());
tmp.set2(m_num2 + mc.get2());
return tmp;
}
};
int main()
{
MyClass a, b, c;
a.set1(1);
a.set2(2);
b.set1(3);
b.set2(5);
// c = a.operator+(b); // 本质上的形式,下面是一般使用形式
c = a + b;
std::cout << c.get1() << " " << c.get2() << std::endl;
}
全局函数实现
#include <iostream>
class MyClass
{
private:
int m_num1;
int m_num2;
public:
MyClass()
{
m_num1 = 0;
m_num2 = 0;
}
void set1(int num)
{
m_num1 = num;
}
void set2(int num)
{
m_num2 = num;
}
int get1()
{
return m_num1;
}
int get2()
{
return m_num2;
}
};
MyClass operator+(MyClass &mc1, MyClass &mc2)
{
MyClass tmp;
tmp.set1(mc1.get1() + mc2.get1());
tmp.set2(mc1.get2() + mc2.get2());
return tmp;
}
int main()
{
MyClass a, b, c;
a.set1(1);
a.set2(2);
b.set1(3);
b.set2(5);
// c = operator+(a, b); // 本质上的形式,下面是一般使用形式
c = a + b;
std::cout << c.get1() << " " << c.get2() << std::endl;
}
<< 重载
前面的例子实现了两个对象相加,实际内部两个数对应相加,但是要输出显示时要手动取获取两个值比较麻烦,所以这里设法重载 <<,实现直接输出显示对象内容。
这里重载不同于前面,如果使用成员函数重载的形式,必须有一个参数,那么使用时就是 “对象1.operator<<(对象2)” 的实质形式了。这里使用需要的形式是 “std::cout << 对象”,所以得使用全局函数来重载,此时必须要有两个参数。第一个参数其实就是要引用 std::cout(它其实是 std::ostream 类型的输出流),第二参数接要显示内容的对象,要显示的内容流入到引用的 std::cout 中,再将它的引用返回,则外部调用时可以继续在它后面 << 添加要显示的内容。
#include <iostream>
class MyClass
{
private:
int m_num1;
int m_num2;
public:
MyClass()
{
m_num1 = 0;
m_num2 = 0;
}
void set1(int num)
{
m_num1 = num;
}
void set2(int num)
{
m_num2 = num;
}
int get1()
{
return m_num1;
}
int get2()
{
return m_num2;
}
MyClass operator+(MyClass &mc)
{
MyClass tmp;
tmp.set1(m_num1 + mc.get1());
tmp.set2(m_num2 + mc.get2());
return tmp;
}
};
std::ostream &operator<<(std::ostream &cout, MyClass &mc)
{
cout << "num1 " << mc.get1() << ",num2 " << mc.get2();
return cout;
}
int main()
{
MyClass a, b, c;
a.set1(1);
a.set2(2);
b.set1(3);
b.set2(5);
// c = a.operator+(b); // 本质上的形式,下面是一般使用形式
c = a + b;
std::cout << c << std::endl;
}
++ 重载
#include <iostream>
class MyInt
{
private:
int m_num;
public:
MyInt(int num)
{
m_num = num;
}
int get()
{
return m_num;
}
MyInt &operator++() // 前置
{
++m_num;
return *this; // 返回对象自己
}
MyInt operator++(int) // 后置
{
MyInt tmp = *this; // 先保存旧对象中的值
++m_num;
return tmp;
}
};
std::ostream &operator<<(std::ostream &cout, MyInt mi)
{
cout << mi.get();
return cout;
}
int main()
{
MyInt mi(8);
std::cout << ++mi << std::endl;
std::cout << mi++ << std::endl;
std::cout << mi << std::endl;
}
= 重载
赋值重载这里考虑的情况和拷贝构造类似,如果不涉及使用堆区,默认的浅拷贝赋值操作也能满足要求。如果有在堆区开辟内存,那么就得实现深拷贝了。
= 重载只能通过成员函数实现
#include <iostream>
class MyClass
{
private:
int *m_num = NULL;
public:
MyClass()
{
m_num = new int(0);
}
MyClass(int num)
{
if (m_num != NULL)
{
delete m_num;
m_num = NULL;
}
m_num = new int(num);
}
int get()
{
return *m_num;
}
~MyClass()
{
if (m_num != NULL)
{
delete m_num;
m_num = NULL;
}
}
MyClass &operator=(MyClass &mc)
{
if (m_num != NULL)
{
delete m_num;
m_num = NULL;
}
m_num = new int(mc.get());
return *this;
}
};
std::ostream &operator<<(std::ostream &cout, MyClass &mc)
{
cout << mc.get();
return cout;
}
int main()
{
MyClass mc1(9);
MyClass mc2, mc3;
mc3 = mc2 = mc1;
std::cout << mc3 << " " << mc2 << std::endl;
}
> 重载
这里举一个例子,定义一个类,有名字和年龄两个属性,重载大于符号实现两个对象的年龄比较。
#include <iostream>
class Person
{
private:
std::string m_name;
int m_age;
public:
Person(std::string name, int age)
{
m_name = name;
m_age = age;
}
int getAge()
{
return m_age;
}
bool operator>(Person &p)
{
if (m_age > p.getAge())
{
return true;
}
return false;
}
};
int main()
{
Person p1("小明", 20);
Person p2("小红", 19);
Person p3("小强", 21);
std::cout << (p1 > p2) << std::endl;
std::cout << (p1 > p3) << std::endl;
}
() 重载
函数调用运算符 () 重载,也叫做仿函数
#include <iostream>
class Add
{
public:
int operator()(int num1, int num2)
{
return num1 + num2;
}
};
class Print
{
public:
void operator()(std::string s)
{
std::cout << s << std::endl;
}
};
int main()
{
Add add;
std::cout << add(9, 1) << std::endl; // 调用仿函数
std::cout << Add()(1, 99) << std::endl; // 匿名对象-仿函数
Print print;
print("Hello world!"); // 调用仿函数
}