数据结构与C++基础汇总(三)

面试所需的数据结构、C++基础知识与细节问题记录专帖(接上帖)。

struct与class有什么区别

类的public、protected、private小结

  • public:可以被该类中的函数、子类的函数、友元函数、该类的对象访问
  • private:只能被该类中的函数、友元函数访问,该类对象无法访问
  • protected:可以被该类中的函数、子类的函数、友元函数,该类对象无法访问

类的继承后方法属性的变化:
private属性不能被继承。

  • 使用private继承:父类的protected和public属性在子类中变为private
  • 使用protected继承:父类的protected和public属性在子类中变为protected
  • 使用public继承:父类的protected和public属性在子类中不改变

C++类的构造函数&析构函数小结

构造函数可以被重载,因为构造函数可以有很多个,且可以带不同的参数。
析构函数不可以被重载,因为析构函数只能有一个,且不可以带参数。

1、作用

1
2
3
4
5
6
7
8
9
10
11
12
13
class Counter
{
public:
// 类Counter的构造函数
// 特点:以类名作为函数名,无返回类型
Counter()
{
m_value = 0;
}

private:
int m_value; // 数据成员
}

该类对象被创建时,编译系统对象分配内存空间,并自动调用该构造函数,由构造函数完成成员的初始化工作。

2、构造函数的种类

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class Complex 
{
private :
double m_real;
double m_imag;
public:
// 无参数构造函数
// 如果创建一个类你没有写任何构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做
// 只要你写了一个下面的某一种构造函数,系统就不会再自动生成这样一个默认的构造函数,如果希望有一个这样的无参构造函数,则需要自己显示地写出来
Complex(void)
{
m_real = 0.0;
m_imag = 0.0;
}

// 一般构造函数(也称重载构造函数)
// 一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)
// 例如:你还可以写一个 Complex( int num)的构造函数出来
// 创建对象时根据传入的参数不同调用不同的构造函数
Complex(double real, double imag)
{
m_real = real;
m_imag = imag;
}

// 复制构造函数(也称为拷贝构造函数)
// 复制构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中
// 若没有显示的写复制构造函数,则系统会默认创建一个复制构造函数,但当类中有指针成员时,由系统默认创建该复制构造函数会存在风险,具体原因请查询有关 “浅拷贝” 、“深拷贝”的文章论述
Complex(const Complex & c)
{
// 将对象c中的数据成员值复制过来
m_real = c.m_real;
m_img = c.m_img;
}

// 类型转换构造函数,根据一个指定的类型的对象创建一个本类的对象
// 例如:下面将根据一个double类型的对象创建了一个Complex对象
Complex::Complex(double r)
{
m_real = r;
m_imag = 0.0;
}

// 等号运算符重载
// 注意,这个类似复制构造函数,将=右边的本类对象的值复制给等号左边的对象,它不属于构造函数,等号左右两边的对象必须已经被创建
// 若没有显示的写=运算符重载,则系统也会创建一个默认的=运算符重载,只做一些基本的拷贝工作
Complex &operator=(const Complex &rhs)
{
// 首先检测等号右边的是否就是左边的对象本,若是本对象本身,则直接返回
if ( this == &rhs )
{
return *this;
}

// 复制等号右边的成员到左边的对象中
this->m_real = rhs.m_real;
this->m_imag = rhs.m_imag;

// 把等号左边的对象再次传出
// 目的是为了支持连等 eg: a=b=c 系统首先运行 b=c
// 然后运行 a= ( b=c的返回值,这里应该是复制c值后的b对象)
return *this;
}
};

void main()
{
// 调用了无参构造函数,数据成员初值被赋为0.0
Complex c1,c2;

// 调用一般构造函数,数据成员初值被赋为指定值
Complex c3(1.0,2.5);
// 也可以使用下面的形式
Complex c3 = Complex(1.0,2.5);

// 把c3的数据成员的值赋值给c1
// 由于c1已经事先被创建,故此处不会调用任何构造函数
// 只会调用 = 号运算符重载函数
c1 = c3;

// 调用类型转换构造函数
// 系统首先调用类型转换构造函数,将5.2创建为一个本类的临时对象,然后调用等号运算符重载,将该临时对象赋值给c2
c2 = 5.2;

// 调用拷贝构造函数( 有下面两种调用方式)
Complex c5(c2);
Complex c4 = c2; // 注意和 = 运算符重载区分,这里等号左边的对象不是事先已经创建,故需要调用拷贝构造函数,参数为c2

}

3、派生类的构造函数
派生类的对象能访问基类的普通成员函数,但无法访问基类构造函数,换句话说就是构造函数不能被继承。解决办法就是在派生类的构造函数中调用基类的构造函数

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
#include<iostream>
using namespace std;

//基类People
class People{
protected:
char *m_name;
int m_age;
public:
People(char*, int);
};
People::People(char *name, int age): m_name(name), m_age(age){}

//派生类Student
class Student: public People{
private:
float m_score;
public:
Student(char *name, int age, float score);
void display();
};

//People(name, age)就是调用基类的构造函数
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
void Student::display(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<"。"<<endl;
}

int main(){
Student stu("小明", 16, 90.5);
stu.display();
return 0;
}

构造函数后面跟冒号,就相当于系统创建成员变量并且初始化,即系统为成员变量分配了一块内存并且把相应的数据给填了进去。

4、析构函数
创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作,例如释放分配的内存、关闭打开的文件等,这个函数就是析构函数。

析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。

注意1:析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。如果用户没有定义,编译器会自动生成一个默认的析构函数。

注意2:只有在一个类被用来做基类的时候,才会把析构函数携程vierual的。

执行时机:

  • 在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数
  • 在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数
  • new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行

后构造的先析构,先构造的后析构。局部变量在结束执行时析构。派生类对象析构时,先执行派生类的析构函数,再执行基类的析构函数。

初始化列表的顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

class obj{
public:
obj(int k):j(k),i(j)
{
}
private:
int i;
int j;
}

int main(){
obj obj(2);
return 0;
}

构造函数初始化列表的初始化顺序,与变量定义顺序有关。因此obj(2)在构造时,i先定义,则先用j初始化i(j没有初始化,为随机数);再用k初始化j为2。

C++类的static理解

参考博客

类中的静态成员或方法不属于类的实例,而属于类本身,并且在类的所有实例间共享。
调用静态成员或静态方法时,应该用类名加操作符“::”来引用。
注意:局部的静态变量使用完便被销毁。

使用静态成员的优势(相比于全局对象):

  • 不会与程序中其他全局名字冲突
  • 静态数据成员可以隐藏信息(不是很懂)

C++类的static成员函数的理解

静态成员函数和静态成员变量一样,不属于类的对象,不包含this指针,不能调用类的非静态成员变量。

初始化列表与构造函数中赋值的使用方法

两者结果是相同的,不同之处就在于:初始化列表可以初始化成员变量,初始化函数需要在函数体中对成员变量进行复制。

const与reference变量不可以被赋值,只能用初始化列表初始化。

子类调用父类的构造函数时,也只能用初始化列表初始化成员变量。

复制构造函数

##