【笔记】C++的类和对象

前言

C++的类和对象学习笔记

定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class 类名
{
// 属性的权限
权限修饰符:
// 定义属性
数据类型 属性名;

// 行为的权限
权限修饰符:
// 定义行为
返回值类型 函数名(参数列表)
{
return 返回值;
}
};

创建对象

1
类名 对象名;

获取对象的属性

1
对象名.属性名;

为对象的属性赋值

1
对象名.属性名 = 值;

调用对象的行为

1
对象名.方法名(参数列表);

权限修饰符

  • 权限修饰符可以加在属性前或方法前
  • 权限修饰符定义一次后,权限修饰符之后的所有属性和函数都被修饰
关键字 权限 类内是否可以访问成员 类外是否可以访问成员变量 是否可以被继承
public 公共权限
protected 保护权限 ×
private 私有权限 × ×
1
2
3
4
5
6
7
8
9
10
11
12
class 类名
{
public:
数据类型 公共权限的变量名;
数据类型 公共权限的变量名;
protected:
数据类型 保护权限的变量名;
数据类型 保护权限的变量名;
private:
数据类型 私有权限的变量名;
数据类型 私有权限的变量名;
};

struct和class

  • struct和class的默认访问权限不同
    • struct的默认访问权限是public公共权限
    • class的默认访问权限是private私有权限

封装

  • 将成员属性私有化,成员函数公有化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class 类名
{
private:
int global_id;

public:
int getId()
{
return global_id;
}

void setName(int id)
{
global_id = id;
}
};

对象的初始化和清理

  • 在类中定义构造函数用来对象的初始化
  • 在类中定义析构函数用来对象的清理
  • 如果定义类的时候没有定义构造函数和析构函数,那么编译器会提供空的构造方法和析构方法

构造函数

定义构造函数

  • 在创建对象时自动执行构造方法
  • 构造函数没有返回值也不写void
  • 构造函数的方法名与类名相同
  • 构造函数可以有参数列表,所以可以有重载方法
定义无参构造
1
2
3
4
5
6
7
8
class 类名
{
public:
类名()
{
...
}
};
定义有参构造
1
2
3
4
5
6
7
8
class 类名
{
public:
类名(参数列表)
{
...
}
};
定义拷贝构造
  • 定义有惨构造,参数是本类的对象
1
2
3
4
5
6
7
8
9
10
class 类名
{
数据类型 属性名;

public:
类名(const 类名 & 对象名)
{
属性名 = 对象名.属性名;
}
};

调用构造函数

  • 由于创建对象时,会自动调用构造函数,调用构造函数的操作实际上是创建对象的操作

  • 注意事项

    • 括号法不要加空括号,会被误认为是函数的声明
    • 匿名对象在当前行执行结束后,系统会立即回收。不能用拷贝函数初始化匿名对象,因为编译器会把类名(对象名)转换成类名 对象名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 调用无参构造(默认构造) */
类名 对象名;

/* 调用有惨构造 */
// 括号法
类名 对象名(参数列表);

// 显式法
类名 对象名 = 类名(参数列表);

// 隐式法,相当于`类名 对象名 = 类名(参数);`
类名 对象名 = 参数;

/* 匿名对象 */
类名(参数列表);

析构函数

  • 在对象销毁前自动执行析构方法
  • 析构函数没有返回值也不写void
  • 析构函数的方法名在类名前加~
  • 析构函数没有参数列表,所以没有重载方法
1
2
3
4
5
6
7
8
class 类名
{
public:
~类名()
{
...
}
};

拷贝构造函数的调用时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义一个有拷贝构造函数的类
class Persion
{
public:
Persion(int i)
{
...
}
Persion(const 类名 & 对象名)
{
...
}
};

Persion method(Persion p)
{
Persion p1(10);
return p1; // 当函数的返回值为局部对象时,此时会调用拷贝构造函数
}

void main()
{
Persion p1(10);
Persion p2(p1); // 用已创建的对象创建新对象时,此时会调用拷贝构造函数
method(p1); // 当函数的参数为值传递时,此时会调用拷贝构造函数
}

默认构造函数

  • 只要定义一个类,就默认存在的函数:
    • 默认构造函数,无参且函数体为空
    • 默认析构函数,函数体为空
    • 默认拷贝构造函数,函数体中对属性值进行拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
class 类名
{
// 默认构造函数
类名() {}
// 默认拷贝构造函数(值拷贝)
类名(类名 对象名)
{
// 对属性值进行拷贝
...
}
// 默认析构函数
~类名() {}
};
  • 如果自定义了有参构造函数,C++不会提供无参构造函数,但是会提供拷贝构造函数
  • 如果自定义了拷贝构造函数,C++不会提供无参构造函数和其他构造函数

深拷贝与浅拷贝

  • 如果拷贝的属性没有指针,那么使用浅拷贝即可
  • 如果拷贝的属性有指针,那么需要手动定义深拷贝来使用

浅拷贝

  • 编译器默认提供的拷贝构造函数是浅拷贝,浅拷贝的实质是把成员属性的值拷贝一份
  • 当拷贝的成员属性是指针的时候,浅拷贝只会拷贝一份指针数据,不会在堆内存中创建新的空间,所带来的问题是,释放指针时会出现堆区内存重复释放的错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Persion
{
int * id;

Persion(const Persion & p)
{
id = p.id;
}
~Persion()
{
if (id != NULL)
{
delete id;
}
}
};

深拷贝

  • 深拷贝需要自定义一个新的拷贝函数,深拷贝会在堆区开辟一块新的空间
  • 当拷贝的成员属性是指针的时候,深拷贝会堆区开辟一块新的空间,所以存放的是一个新的指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Persion
{
int * id;

Persion(const Persion & p)
{
id = new int(*p.id);
}
~Persion()
{
if (id != NULL)
{
delete id;
}
}
};

初始化列表

  • 初始化列表是另一种为成员属性赋初值的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Persion
{
int global_id
int global_num

// 传统的构造函数赋初值
Persion(int id, int num)
{
global_id = id;
global_num = num;
}

// 初始化列表赋初值
Persion(int id, int num): global_id(id), global_num(num)
{

}

};

类对象作为类成员

  • 将一个类的对象作为另一个类的成员属性
1
2
3
4
5
6
7
8
9
class Son
{

};

class Father
{
Son son;
};

静态成员

  • 在成员属性和成员函数前添加static关键字,就构成了静态成员
  • 静态成员变量也受访问权限的制约

静态成员变量

  • 所有对象共享一份数据
  • 在编译阶段分配内存
  • 类内声明,类外初始化
1
2
3
4
5
6
class 类名
{
static 数据类型 属性名;
};

数据类型 Persion::属性名 = 值;

静态成员变量的访问

通过对象访问
1
2
类名 对象名;
对象名.属性名;
通过类名访问
1
类名.属性名;

静态成员函数

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量,非静态成员变量无法访问
1
2
3
4
5
6
7
class 类名
{
static 返回值类型 函数名()
{
...
}
};

静态成员函数的访问

通过对象访问
1
2
类名 对象名;
对象名.函数名();
通过类名访问
1
类名::函数名();

this指针

  • this指针是隐含每一个非静态成员函数内的一种指针,this指针不需要定义可以直接使用
  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态函数中返回对象本身,可用*this
1
2
3
4
5
6
7
8
9
10
11
12
13
class Persion
{
int id;
Persion(int id)
{
this->id = id;
}

Persion& method()
{
return *this;
}
};

常函数与常对象

常函数

  • 声明函数时添加const关键字
  • 常函数内不可修改成员属性(相当于this指针改为了常量)
  • 常函数内只能调用常函数
1
2
3
4
返回值类型 函数名 const
{
retuen 返回值;
}

常对象

  • 声明对象时添加const关键字
  • 常对象只能调用常函数
1
const 数据类型 对象名;

常函数和常对象都能修改的成员属性

  • 如果需要修改成员属性,可以在成员属性声明时添加mutable关键字,使得常含数内的成员属性可以被修改
1
mutable 数据类型 成员属性;

类外定义成员函数

定义普通函数

1
2
3
4
5
6
7
8
9
class Persion
{
void method();
};

void Persion::method()
{
...
}

定义构造函数

  • 在类内声明成员函数,类外定义成员函数
1
2
3
4
5
6
7
8
9
class Persion
{
Persion();
};

Persion::Persion()
{
...
}

友元

  • 友元的目的是让一个函数或者类访问另一个类的私有成员

成员函数做友元

  • 通过friend关键字在类中定义其他函数作为友元
1
2
3
4
5
6
7
8
9
10
11
12
class 类名
{
friend 返回值类型 函数名(类名 &对象名);

private:
int id;
};

返回值类型 函数名(类名 &对象名)
{
对象名.id;
}

类做友元

  • 通过friend关键字在类中定义其他类作为友元
1
2
3
4
5
6
7
8
9
10
11
12
class 类名
{
friend class Friend;

private:
int id;
};

class Friend
{
...
};

成员函数做友元

  • 通过friend关键字在类中定义其他类中的函数作为友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class 类名
{
friend class Friend::method();

private:
int id;
};

class Friend
{
void method()
{
...
}
};

运算符重载

算数运算符重载

  • 此处以加法的运算符重载为例
  • 为了能实现连续运算,返回值类型为类对象

+:指定被重载的运算符,这里重载的运算符是加法

通过成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Persion
{
private:
int id;

public:
Persion() {}
Persion(int id)
{
this->id = id;
}

Persion operator+(Persion &persion)
{
Persion temp;
temp.id = this->id + persion.id;
return temp;
}
};

int main()
{
Persion p1(1);
Persion p2(1);

// 简化前
//Persion p3 = p1.operator+(p2);
// 简化后
Persion p3 = p1 + p2;

return 0;
}

通过全局函数

  • 直接在运算符重载函数的参数列表制定两个类型的参数,可以是不同类型的数据(例如自定义数据类型与int数据类型)
  • 内置的数据类型是不可以改变的(例如两个int类型数据相加)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Persion
{
friend Persion operator+(Persion &persion1, Persion &persion2);

private:
int id;

public:
Persion() {}
Persion(int id)
{
this->id = id;
}
};

Persion operator+(Persion &persion1, Persion &persion2)
{
Persion temp;
temp.id = persion1.id + persion2.id;
return temp;
}

int main()
{
Persion p1(1);
Persion p2(1);

// 简化前
//Persion p3 = operator+(p1, p2);
// 简化后
Persion p3 = p1 + p2;

return 0;
}

左移运算符重载

  • 可以实现输出任意自定义数据类型的数据
  • 左移运算符重载的函数返回值为空,函数体内拼接需要输出的内容

通过全局函数

  • 因为封装的时候通常将成员变量私有化,所以为了能让左移运算符重载函数可以访问私有成员变量,通常在类内定义友元
  • 为了能实现连续左移,返回值类型为ostream类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Persion
{
friend ostream & operator<<(ostream &cout, Persion &persion);

private:
int id;

public:
Persion(int id)
{
this->id = id;
}
};

ostream& operator<<(ostream &cout, Persion &persion)
{
cout << "id=" << persion.id;
return cout;
}

int main()
{
Persion p(1);

cout << p << endl;

return 0;
}

递增运算符重载

  • 前置递增
    • 为了能实现递增的嵌套,返回值类型需要设置为引用类型
  • 后置递增
    • 返回值类型需要设置为值
    • 为了能实现递增的嵌套,后置递增运算符重载函数的参数需要用int作为占位参数,且只能是int,用于区分前置运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Persion
{
private:
int id;

public:
Persion(int id)
{
this->id = id;
}

// 前置自增运算符重载
Persion& operator++()
{
// 先递增
this->id++;
// 后返回
return *this;
}

// 后置自增运算符重载
Persion operator++(int)
{
// 先记录
Persion temp = *this;
// 然后递增
this->id++;
// 最后返回
return temp;
}
};

int main()
{
Persion p(1);
p++;
++p;

return 0;
}

赋值运算符重载

  • 默认的赋值运算符是浅拷贝数据,重载赋值运算符
  • 为了能实现连等,返回值类型为类对象的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Persion
{
private:
int *id;
public:
Persion(int id)
{
this->id = new int(id);
}

Persion& operator=(Persion &persion)
{
if (this->id != NULL)
{
delete this->id;
this->id = NULL;
}
this->id = new int(*persion.id);
return *this;
}
};

int main()
{
Persion p1(1);
Persion p2 = p1;

return 0;
}

关系运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Persion
{
private:
int id;

public:
Persion(int id)
{
this->id = id;
}

bool operator==(Persion &persion)
{
return this->id == persion.id ? true : false;
}
};

int main()
{
Persion p1(1);
Persion p2(1);

p1==p2;

return 0;
}

函数调用运算符重载

  • 函数调用运算符(())也可以重载,由于重载后使用的方式非常像函数的调用,因此称为仿函数,仿函数没有固定写法,非常灵活
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Persion
{
public:
Persion() {}

返回值类型 operator()(参数列表)
{
return 返回值;
}
};

int main()
{
Persion persion;
persion(参数);

// 匿名函数对象
Persion()(参数);

return 0;
}

继承

  • 将重复的代码定义为父类(基类),子类(派生类)继承父类,减少重复的代码
  • 父类中所有非静态的成员属性都会被子类继承
  • 父类中的私有成员属性无法被子类访问,但是确实被继承了,只是被编译器所隐藏了

权限修饰符

public:公共继承
pritected:保护继承
private:私有继承

1
2
3
4
5
class Father
{};

class Child : 权限修饰符 Father
{};

继承的分类

公共继承

  • 公共继承的子类,无法继承父类的private成员属性
  • 公共继承的子类,继承到的public成员属性还是public权限
  • 公共继承的子类,继承到的protected成员属性还是protected权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Father
{
public:
int a;
protected:
int b;
private:
int c;
};

class Child : public Father
{
public:
int a;
protected:
int b;
};

保护继承

  • 保护继承的子类,无法继承父类的private成员属性
  • 保护继承的子类,继承到的public成员属性和protected成员属性都变为protected权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Father
{
public:
int a;
protected:
int b;
private:
int c;
};

class Child : protected Father
{
protected:
int a;
int b;
};

私有继承

  • 保护继承的子类,无法继承父类的private成员属性
  • 保护继承的子类,继承到的public成员属性和protected成员属性都变为private权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Father
{
public:
int a;
protected:
int b;
private:
int c;
};

class Child : private Father
{
private:
int a;
int b;
};

报告单个类布局

  • 利用Visual Studio开发人员命令提示符报告单个类布局

  • 打开Visual Studio开发人员命令提示符

<class>:类名
<file>:C++源码文件

1
cl /d1 reportSimgleClassLayout<class> <file>

访问父子类同名属性和函数

  • 子类对象可以直接访问到之类中同名成员
  • 子类对象加作用域可以访问到父类同名成员
  • 子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数(无论是否是重载的成员函数),如果想访问到父类中被隐藏的同名成员函数,需要强制加作用域

非静态成员属性和函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Father
{
int id;
void method() {}
void method(int) {}
};

class Son : public Father
{
int id;
void method() {}
};

int main()
{
Son son;

/* 子类访问成员属性 */
// 子类访问子类属性
son.id;
// 子类访问父类属性
son.Father::id;

/* 子类访问成员函数 */
// 子类访问子类函数
son.method();
// 子类访问父类函数
son.Father::method();
son.Father::method(1);

return 0;
}

静态成员属性和函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Father
{
static int id;
void method() {}
};
int Father::id =1;

class Son : public Father
{
static int id;
void method() {}
};
int Son::id =1;

int main()
{
Son son;

/* 子类通过类对象访问成员属性 */
// 子类访问子类属性
son.id;
// 子类访问父类属性
son.Father::id;

/* 子类通过类名访问成员属性 */
// 子类访问子类属性
Son::id;
// 子类访问父类属性
Son::Father::id;

/* 子类通过对象访问成员函数 */
// 子类访问子类属性
son.method();
// 子类访问父类属性
son.Father::method();

/* 子类通过类名访问成员函数 */
// 子类访问子类属性
Son::method();
// 子类访问父类属性
Son::method();

return 0;
}

多继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Father
{
int id;
};

class Mother
{
int id;
};

class Son : public Father, public Mother
{

};

菱形继承

  • 菱形继承,又称钻石继承
  • 两个子类继承同一个父类,一个孙子类又继承这两个子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class GrandFather
{
int id;
};

class Father : public GrandFather
{
int id;
};

class Mother : public GrandFather
{
int id;
};

class Son : public Father, public Mother
{
int id;
};

int main()
{
Son son;
son.Father::id;
son.Method::id;
}

虚继承

  • 为了解决菱形继承遇到的问题,引出了虚继承
    • 菱形继承遇到的问题:孙子类中包含了重复的同名成员属性,出自于两个父类,这两个父类由于继承自相同的爷爷类,所以同名成员属性传递给孙子类的时候,造成了资源的浪费
  • 虚继承采用virtual关键字修饰基类,这种基类成为虚基类
  • 虚继承能解决资源的浪费,采用了虚继承,继承时重复的同名成员属性会变成共享的成员属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class GrandFather
{
int id;
};

class Father : virtual public GrandFather
{
int id;
};

class Mother : virtual public GrandFather
{
int id;
};

class Son : public Father, public Mother
{
int id;
};

int main()
{
Son son;
son.Father::id;
son.Method::id;
}

多肽

多肽的分类

  • 静态多肽:函数重载和运算符重载都属于静态多肽

  • 动态多肽:派生类和虚函数实现运行时多肽

  • 静态多肽和动态多肽的区别:静态多肽的函数地址在编译阶段确定;动态多台的函数地址在运行阶段确定

虚函数

  • 父类引用(指针)指向子类对象

  • 利用virtual关键字修饰函数,被称之为虚函数,虚函数可以实现多肽

  • 动态多肽的条件

    • 有继承关系
    • 子类需要重写父类的虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Father
{
virtual void method() {}
};

class Son : public Father
{
void method() {}
};

int main()
{
Father father = new Son();
father.method();

return 0;
}

纯虚函数和抽象类

  • 通常父类中虚函数的实现是无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
  • 包含纯虚函数的类,就是抽象类,抽象类无法实例化对象,继承抽象类的子类必须重写纯虚函数,如果不重写纯虚函数,那么子类也是抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Father
{
virtual void method() = 0;
};

class Son : public Father
{
void method() {}
};

int main()
{
Father father = new Son();
father.method();

return 0;
}

虚析构函数

  • 如果子类中有属性开辟堆区,父类指针无法在释放时调用到子类的析构函数,如果想通过父类调用子类虚构函数,需要采用虚析构函数
  • 虚析构函数与普通的析构函数不同,父类的虚析构函数在被子类调用时,父类的虚析构函数和子类的析构函数都会被调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Father
{
vertial ~Father() {}
};

class Son : public Father
{
~Son() {
...
}
};

int main()
{
Father *father = new Son();

return 0;
}

纯虚析构函数

  • 如果只是用父类释放子类堆中的属性,那么父类的虚析构不需要函数体,此时可以使用纯虚析构函数
  • 虽然父类析构函数有时只是释放子类堆中的属性,不需要函数体,但是父类的虚析构函数仍然有可能释放父类堆中的属性,所以仍然要定义父类析构函数的函数体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Father
{
vertial ~Father() = 0;
};

Father::~Father()
{
...
}

class Son : public Father
{
~Son() {
...
}
};

int main()
{
Father *father = new Son();

return 0;
}

完成

参考文献

哔哩哔哩——黑马程序员