目录
1.在成员函数后面加const,变成常函数,不可以修改变量的值
参考链接:27 类和对象-对象特性-深拷贝与浅拷贝_哔哩哔哩_bilibili
一、封装
1.访问权限
public:类内类外都可以访问
protect:类内可以访问,类外不可以访问,子类可以访问
private:类内可以访问,类外不可以访问,子类不可以访问
2.构造函数和析构函数
构造函数:类创建的时候自动执行,用来初始化参数。
析构函数:类消除的时候自动执行,用来释放内存。
3.构造函数的分类和调用
构造函数分类
无参构造:也叫默认构造,注意在使用无参构造的时候,不要加(),不然会被编译器当成函数声明
有参构造:相比于无参构造就是多了个参数。
拷贝构造:实质上是有参构造。
3.1 拷贝构造的使用场景
1.函数值传递
2.函数返回值,对于函数返回值的时候,由于编译器的问题,需要再下面加-fno-elide-constructors,不然拷贝构造会被吞掉。
g++ class_test.cpp -o main -fno-elide-constructors
4.构造函数的默认调用规则
4.1默认情况
对于一个类
如果什么都不写,那么默认提供默认构造(空内容),析构函数(空内容),拷贝构造函数(将对象拷贝)。对于构造函数而言
如果只提供有参构造,那么自动提供拷贝构造。
如果只提供拷贝构造,那么自动什么也不提供。
4.2调用规则
如果只提供有参构造,那么就不能使用默认构造的方式,如Person p; 因为编译器不会自动提供默认构造,需要自己写默认构造。同理,如果只提供了拷贝构造,那么也不能使用Person p;这种,需要自己写默认构造。
5.深拷贝与浅拷贝
编译器默认提供的拷贝函数是浅拷贝。 当程序运行结束之后,会调用析构函数释放空间,由于p1和p2指向的都是同一片空间,但是p1已经把空间给释放掉了,p2又重新释放空间,所以就会报错。
深拷贝,在拷贝的时候重新申请一片区域,释放的时候就不会有问题。
6.初始化列表
无参构造
有参构造
总结:
目前初始化的手段
1.通过构造函数,可以在构造函数里面初始化,或者使用构造函数列表的方式
2.不适用构造函数,对于public的属性,直接在类外赋值。
7.类对象作为类成员
假设有class A,A中有class B,那么构造函数的时候,先构造B后构造A,析构的时候,先析构A,再析构B。
8.静态成员
静态成员就是在成员变量和成员函数前面加上关键字static,称为静态成员。
1.静态成员变量
(1)类内声明,类外初始化
(2)所有对象共享一份内存(不占用对象本身内存)
这意味着使用对象还有使用类都可以访问到
也意味着,使用一个对象对其进行更改,其他对象的这个属性也会变化。
(3)在编译阶段进行内存分配
注意事项,静态成员变量也有访问权限
2.静态成员函数
(1)所有对象公用
(2)只能操作静态成员变量
9.成员变量与成员函数分开储存
(1)空对象的占用1字节
(2)变量属于对象上
(3)静态变量不属于对象上
(4)函数不属于对象上
(5)静态函数不属于对象上
10.this指针
1.this指针的性质
this指针指向被调用成员函数所属的对象
this指针不需要定义,隐含在每一个非静态成员函数里面。
this指针是指针常量,指向的位置不可以修改
2.this指针的用途
(1)当形参和成员变量重名的时候
(2)在类的非静态成员函数返回对象本身
11.空指针调用成员函数
空指针可以调用成员函数,但是如果成员函数里面有对成员变量的操作就会出错,因为成员变量本质上是this->成员变量,由于this是空指针,所以有问题。
12.const修饰成员函数
1.在成员函数后面加const,变成常函数,不可以修改变量的值
2.给变量加上mutable,可以修改变量的值
3.常对象不可以修改变量的值
4.在对象前面加上mutable可以修改
5.常对象只能调用常函数
因为常对象不能改变变量的值(除mutable),而常函数也不能改变,其他函数可以改变,所以常对象只能调用常函数。
13.友元(这里涉及了类外实现成员函数)
(1)全局函数做友元
全局函数做友元访问私有属性。
(2)友元类
# include <iostream>
class Person
{
friend class Friend;
public:
Person();
~Person()
{
}
int a;
private:
int b;
};
Person::Person()
{
a = 10;
b = 20;
}
class Friend
{
public:
Friend();
~Friend()
{
}
void visit();
Person * p1;
};
Friend::Friend()
{
p1 = new Person;
}
void Friend::visit()
{
std::cout << p1->a << std::endl;
std::cout << p1->b << std::endl;
}
void dowork()
{
const Person p1;
Friend f1;
f1.visit();
}
int main()
{
dowork();
return 0;
}
(3)成员函数做友元
#include <iostream>
class Person; // 前置声明
class Friend
{
public:
Friend();
~Friend()
{
}
void visit();
Person * p1;
};
class Person
{
friend void Friend::visit(); // 现在 Friend 已声明
public:
Person();
~Person()
{
}
int a;
private:
int b;
};
Person::Person()
{
a = 10;
b = 20;
}
Friend::Friend()
{
p1 = new Person;
}
void Friend::visit()
{
std::cout << p1->a << std::endl;
std::cout << p1->b << std::endl; // 这里可访问 private 成员
}
void dowork()
{
Friend f1;
f1.visit();
}
int main()
{
dowork();
return 0;
}
14.类重载运算符
(1)加号运算符重载
(2)左移运算符重载
为什么这个重载运算符需要卸载函数外面,用全局函数,原因是如果写在函数里面,那么大概就是
void operator<<(std::cout), 这个外面调用的时候就应该写 p << std::cout ,这样不合适。 所有的重载运算符的展开行驶都是 p 运算符号 参数。 对于加号就是 p + p1。
(3)递增运算符重载
前置++
#include<iostream>
class Myinter
{
// 声明友元函数,使其可以访问私有成员
friend std::ostream & operator<<(std::ostream & os, Myinter &myinter);
public:
// 构造函数,初始化 num 为 10
Myinter(): num(10)
{
}
// 前置自增运算符重载
Myinter & operator++()
{
num++;
return *this;
}
int num;
};
// 重载 << 运算符,使用传入的输出流对象 os
std::ostream & operator<<(std::ostream & os, Myinter &myinter)
{
// 使用传入的 os 输出 num 的值
os << myinter.num;
return os;
}
void test()
{
// 创建 Myinter 类的对象 myinter
Myinter myinter;
// 对对象 myinter 应用前置自增运算符并输出
std::cout << ++myinter << std::endl;
}
int main()
{
test();
return 0;
}
后置++
#include<iostream>
class Myinter
{
// 声明友元函数,使其可以访问私有成员
friend std::ostream & operator<<(std::ostream & os, Myinter &myinter);
public:
// 构造函数,初始化 num 为 10
Myinter(): num(10)
{
}
// 前置自增运算符重载
Myinter & operator++()
{
num++;
return *this;
}
Myinter & operator++(int)
{
// 后置自增运算符重载,先返回当前对象,再自增
Myinter temp = *this;
num++;
return temp;
}
int num;
};
// 重载 << 运算符,使用传入的输出流对象 os
std::ostream & operator<<(std::ostream & os, Myinter &myinter)
{
// 使用传入的 os 输出 num 的值
os << myinter.num;
return os;
}
void test()
{
// 创建 Myinter 类的对象 myinter
Myinter myinter;
myinter++;
myinter++;
// 对对象 myinter 应用前置自增运算符并输出
std::cout << myinter << std::endl;
}
int main()
{
test();
return 0;
}
(4)赋值运算符重载
每个自定义类都有自己的初始赋值运算符,他会将一个对象的所有属性拷贝到另外一个对象的所有属性,但是是浅拷贝,所以释放的时候可能有问题。
所以下面重载赋值运算符,进行深拷贝
注意:不要Person p2 = p1,这个时候调用的是拷贝构造函数,而不是对=进行重载
返回是当前对象的引用,意味着可以一直=下去。
#include<iostream>
class Person
{
public:
Person(int a)
{
this->a = new int(a);
}
~Person()
{
// std::cout << "析构函数调用" << std::endl;
if(this->a !=NULL)
{
delete a;
a = NULL;
}
}
// 赋值运算符重载,返回引用以支持连续赋值
Person & operator=(const Person & p)
{
this->a = new int(*p.a); // 使用传入对象的值
return *this;
}
int * a;
};
void test()
{
Person p1(10);
Person p2(20);
p2 = p1;
std::cout << p1.a <<std::endl;
std::cout << p2.a <<std::endl;
std::cout << *p2.a <<std::endl;
}
int main()
{
test();
return 0;
}
(5)关系运算符重载
#include<iostream>
class Myinter
{
// 声明友元函数,使其可以访问私有成员
friend std::ostream & operator<<(std::ostream & os, Myinter &myinter);
public:
// 构造函数,初始化 num 为 10
Myinter(): num(10)
{
}
// 前置自增运算符重载
bool operator==(Myinter & p)
{
if (this->num == p.num)
{
return true;
}
else
{
return false;
}
}
int num;
};
void test()
{
// 创建 Myinter 类的对象 myinter
Myinter myinter;
Myinter myinter1;
bool c = myinter == myinter1;
// 对对象 myinter 应用前置自增运算符并输出
std::cout << c << std::endl;
}
int main()
{
test();
return 0;
}
(6)函数调用运算符重载
可以通过对象调用,也可以直接通过类调用
既然这个函数重载卸载类的内部,所以也可以通过这个来调用私有成员
二、继承
1.基本语法
#include <iostream>
class Base
{
public:
void header()
{
std::cout << "header" << std::endl;
}
};
class c: public Base
{
public:
void content()
{
std::cout << " c" << std::endl;
}
};
class python: public Base
{
public:
void content()
{
std::cout << " python" << std::endl;
}
};
void test()
{
c c1;
c1.content();
c1.header();
python python1;
python1.content();
python1.header();
}
int main()
{
test();
}
2.继承方式
继承的语法: class 子类: 继承方式 父类
继承方式有三种,公共继承,保护继承,私有继承
1.公共继承:父类是什么权限,子类还是什么权限
2.保护继承:父类的私有权限不变和保护权限不变,公有权限变成保护权限
3.私有继承:父类的所有成员变量继承到子类全部变私有的
注意:如果子类继承父类的成员变量变成私有的,那么子类无法操作这个成员变量。类内类外不可读不可写。
3.继承中的对象模型
父类中的所有非静态成员变量都会被子类继承下去
4.构造和析构的顺序
先父类构造,子类构造,子类析构,父类析构。
5.同名成员处理
访问子类同名成员直接访问,访问父类同名成员加作用域
(1)成员变量
(2)成员函数(下面这个图有错误,构造函数光声明没定义,但是不重要,我不想改了)
6.同名静态成员的处理
访问子类同名成员直接访问,访问父类同名成员加作用域,和上面一样。
(1)同名静态成员函数
#include <iostream>
class Base
{
public:
Base()
{
a = 10;
}
int a;
static void func()
{
std::cout << "Base func" << std::endl;
}
};
class C: public Base
{
public:
C()
{
}
static void func()
{
std::cout << "C func" << std::endl;
}
};
void test()
{
C c1;
// 通过成员访问符访问静态成员函数
c1.func();
c1.Base::func();
// 通过类名访问静态成员函数
Base::func();
C::func();
}
int main()
{
test();
}
(2)访问同名静态成员变量
#include <iostream>
class Base
{
public:
Base()
{
}
static int a;
};
int Base::a = 10;
class C: public Base
{
public:
C()
{
}
static int a;
};
int C::a = 20;
void test()
{
C c1;
// 通过成员访问符访问静态成员函数
std::cout << c1.a << std:: endl;
std::cout << c1.Base::a << std::endl;
// 通过类名访问静态成员函数
std::cout << Base::a << std:: endl;
std::cout << C::a << std:: endl;
}
int main()
{
test();
}
7.多继承语法
c++允许一个类继承多个类,语法:class 子类:继承方式 父类1, 继承方式 父类2。。。
#include <iostream>
class Base1
{
public:
Base1()
{
a = 10;
}
int a;
};
class Base2
{
public:
Base2()
{
b = 10;
}
int b;
};
class C: public Base1, public Base2
{
public:
C()
{
c = 1;
d = 2;
}
int c;
int d;
};
void test()
{
C c1;
std::cout << sizeof(c1) << std:: endl;
}
int main()
{
test();
}
8.菱形继承问题
如果一个类继承了两个父类,那么他相当于把两个父类的成员变量都拷贝了一份。如果两个父类有相同的成员变量,那么对于这个子类相当于继承了两份这个成员变量,访问的时候可以使用作用域区分。
其实对于上面这个问题,SheepTuo相当于继承了两份Animal,这种没必要,相当于增大了两倍的内存开销,为了解决这个问题,引入虚继承。虚继承在子类中开辟唯一空间,即使继承了两份,只不过存储两个不同的偏移指针。
由于这个虚继承的存在,所以改一发而两个都变。
三、多态
1.多态的基本语法
函数早绑定,因为参数是Animal类,但是传的是子类,调用的时候依然是父类,因为函数早绑定。
在函数前面加上virtual实现函数晚绑定,函数传参的时候引用很关键,如果不是通过引用传参,那么结果不是“猫正在说话”。(其实这个是否说明父类指针指向子类才会发生多态,直接复制不会)
2.多态原理剖析
正常来讲函数是不属于类的一部分的,但是如果在函数前面加上virtual关键字,那么就会在类中产生一个指针,这个指针指向一个虚函数表,这个表就是函数储存的位置。
不要被这个size(4)误导了,因为他的电脑32位的。
当子类继承父类的时候,也会继承这个虚函数指针和虚函数表,指向和父类相同的虚函数,这个时候对子类进行virtual重载,那会就会重写这个虚函数指针和虚函数表,虚函数表里面是重载的函数。当一个父类指针指向子类的时候,调用子类的函数,也就会调用这个虚函数表中重载的函数。
总体:
3.纯虚函数和抽象类
(1)在多态中,父类的虚函数的实现没有意义,因此可以将虚函数改成纯虚函数。纯虚函数的语法:virtual 返回值类型 函数名(参数)=0;
(2)只要类中有纯虚函数,就是抽象类抽象类无法实例化对象
(3)子类继承抽象类就要实现纯虚函数,不然仍然是抽象类。
4.虚析构和纯虚析构
当使用多态的时候,如果子类申请了内存,但是由于函数结束的时候不会调用子类虚构函数,所以会导致内存无法释放,为了解决这个问题引入了虚析构和纯虚析构。
(1) 子类申请内存无法释放
#include <iostream>
#include <string>
class Animal
{
public:
Animal()
{
std::cout << "Animal 构造函数" << std:: endl;
}
~Animal()
{
std::cout << "Animal 析构函数" << std:: endl;
}
virtual void speak() = 0;
};
class Cat: public Animal
{
public:
Cat(std::string name)
{
std::cout << "Cat 构造函数" << std:: endl;
this->name = new std::string(name);
}
~Cat()
{
std::cout << "Cat 析构函数" << std:: endl;
if(this->name!=NULL)
{
delete name;
name = NULL;
}
}
virtual void speak()
{
std::cout << *name << ":猫在说话" << std:: endl;
}
std::string * name;
};
void Dospeak(Animal &animal)
{
animal.speak();
}
void test()
{
Animal* animal = new Cat("Tom");
Dospeak(*animal);
delete animal;
}
int main()
{
test();
}
(2)虚析构函数
#include <iostream>
#include <string>
class Animal
{
public:
Animal()
{
std::cout << "Animal 构造函数" << std:: endl;
}
virtual ~Animal()
{
std::cout << "Animal 析构函数" << std:: endl;
}
virtual void speak() = 0;
};
class Cat: public Animal
{
public:
Cat(std::string name)
{
std::cout << "Cat 构造函数" << std:: endl;
this->name = new std::string(name);
}
~Cat()
{
std::cout << "Cat 析构函数" << std:: endl;
if(this->name!=NULL)
{
delete name;
name = NULL;
}
}
virtual void speak()
{
std::cout << *name << ":猫在说话" << std:: endl;
}
std::string * name;
};
void Dospeak(Animal &animal)
{
animal.speak();
}
void test()
{
Animal* animal = new Cat("Tom");
Dospeak(*animal);
delete animal;
}
int main()
{
test();
}
(3)纯虚构函数
#include <iostream>
#include <string>
class Animal
{
public:
Animal()
{
std::cout << "Animal 构造函数" << std:: endl;
}
virtual ~Animal() = 0;
virtual void speak() = 0;
};
Animal::~Animal()
{
std::cout << "Animal 纯析构函数" << std:: endl;
}
class Cat: public Animal
{
public:
Cat(std::string name)
{
std::cout << "Cat 构造函数" << std:: endl;
this->name = new std::string(name);
}
~Cat()
{
std::cout << "Cat 析构函数" << std:: endl;
if(this->name!=NULL)
{
delete name;
name = NULL;
}
}
virtual void speak()
{
std::cout << *name << ":猫在说话" << std:: endl;
}
std::string * name;
};
void Dospeak(Animal &animal)
{
animal.speak();
}
void test()
{
Animal* animal = new Cat("Tom");
Dospeak(*animal);
delete animal;
}
如果是纯虚析构函数,那么是抽象类,无法实例化对象