前言

为了准备秋招及春招,现阶段的任务是将所学过的知识进行复习,并串联起来。
首当其冲的就是编程语言的复习,C++自己平时写的也比较多,但仍有很多语法细节不太记得了,这里浅记一些平常容易忽略的要点

C++基础入门

一维数组数组名

一维数组名称的用途

  1. 可以统计整个数组在内存中的长度(单位:字节B)
  2. 可以获取数组在内存中的首地址

已知一个数组的数组名,求数组元素的个数,如下:

1
2
3
4
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
cout << "整个数组所占内存空间为: " << sizeof(arr) << endl;
cout << "每个元素所占内存空间为: " << sizeof(arr[0]) << endl;
cout << "数组的元素个数为: " << sizeof(arr) / sizeof(arr[0]) << endl;

可以通过数组名获取到数组首地址:

1
2
3
cout << "数组首地址为: " << (int)arr << endl;
cout << "数组中第一个元素地址为: " << (int)&arr[0] << endl;
cout << "数组中第二个元素地址为: " << (int)&arr[1] << endl;

这里首地址即为第一个元素的地址

指针

指针所占内存空间

所有指针类型在32位操作系统下是4个字节

1
2
3
4
cout << sizeof(int *) << endl; // 4
cout << sizeof(char *) << endl; // 4
cout << sizeof(float *) << endl; // 4
cout << sizeof(double *) << endl; // 4

空指针和野指针

空指针:指针变量指向内存中编号为0的空间

1
int * p = NULL;

用途:初始化指针变量
注意:空指针指向的内存是不可以访问的


野指针:指针变量指向非法的内存空间

1
2
3
4
int * p = (int *)0x1100;

//访问野指针报错
cout << *p << endl;

两者共同点:都不能访问

const修饰指针

const修饰指针有三种情况:

  1. const修饰指针 — 常量指针 (const int * p)
    const修饰的是指针,指针指向可以改,指针指向的值不可以更改

  2. const修饰常量 — 指针常量 (int * const p)
    const修饰的是常量,指针指向不可以改,指针指向的值可以更改

  3. const即修饰指针,又修饰常量 (const int * const p)
    const既修饰指针又修饰常量,两者均不可修改

指针和数组

利用指针访问数组中的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };

int* p = arr; //指向数组的指针

cout << "第一个元素: " << arr[0] << endl; // 1
cout << "指针访问第一个元素: " << *p << endl; // 1
cout << "指针访问第二个元素: " << *(p+1) << endl; // 2

for (int i = 0; i < 10; i++)
{
//利用指针遍历数组
cout << *p << endl;
p++;
}

指针和函数

利用指针作函数参数,可以修改实参的值

1
2
3
4
5
6
7
8
// 地址传递
void swap2(int * p1, int *p2) // 声明与定义
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
swap(&a, &b); // 调用传参

如果是数组名作为函数参数的话,两种写法效果相同,因为数组名就代表了首元素的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void bubbleSort(int * arr, int len)  //int * arr 也可以写为int arr[]
{
for (int i = 0; i < len - 1; i++)
{
for (int j = 0; j < len - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}

结构体

结构体指针

通过指针访问结构体中的成员
利用操作符 -> 可以通过结构体指针访问结构体属性

1
2
3
4
student stu = { "张三",18,100};
student * p = &stu;
p->score = 80; //指针通过 -> 操作符可以访问成员
cout << "姓名:" << p->name << " 年龄:" << p->age << " 分数:" << p->score << endl;

函数参数

结构体作函数参数时,如果不想修改主函数中的数据,用值传递,反之用地址传递

1
2
3
4
// 声明
void printStudent2(student *stu);
// 调用
printStudent2(&stu);

C++核心编程

内存4大分区

C++程序在执行时,将内存大方向划分为4个区域

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的。特点共享只读
  • 全局区:存放全局变量、静态变量、常量,该区域的数据在程序结束后由操作系统释放
  • 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
  • 堆区:由程序员分配和释放内存(new),若程序员不释放(delete),程序结束时由操作系统回收

new操作符

C++中利用new操作符在堆区开辟数据
利用new创建的数据,会返回该数据对应的类型的指针

1
int* a = new int(10); // 创建一个值为10变量

开辟数组

1
2
int* arr = new int[10];
delete[] arr;

引用&(指针常量)

作用:给变量起别名,本质上还是原来那个变量

  • 引用必须初始化
  • 引用在初始化后,不可以改变(易错)

1
2
3
4
5
6
7
8
9
int a = 10;
int b = 20;
//int &c; //错误,引用必须初始化
int& c = a; //一旦初始化后,就不可以更改
c = b; //这是赋值操作,不是更改引用,a的值也会改变

cout << "a = " << a << endl; // 20
cout << "b = " << b << endl; // 20
cout << "c = " << c << endl; // 20

由于c是a的别名,c在被重新赋值后,a也会随之改变
这个特点也印证了,引用本质上是一个指针常量,即指针指向的值可以修改,但是指针的指向不允许修改。

引用作函数参数

可以代替指针,实现函数内修改实参的操作

通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单

1
2
3
4
5
6
void mySwap03(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
mySwap03(a, b);

函数重载

函数名可以相同,提高复用性

函数重载满足条件

  1. 同一个作用域下
  2. 函数名称相同
  3. 函数参数类型、个数、顺序至少要有一个不同

注:函数返回值不可以作为函数重载条件

类与对象

构造/析构函数写法

类的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
public:
Person() {
cout << "无参构造函数!" << endl;
mAge = 0;
}
Person(int age) {
cout << "有参构造函数!" << endl;
mAge = age;
}
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
mAge = p.mAge;
}
//析构函数在释放内存之前调用
~Person() {
cout << "析构函数!" << endl;
}
public:
int mAge;
};

利用类创建对象

1
2
3
Person man1; // 无参构造
Person man2(100); // 有参构建
Person newman(man); // 拷贝构造函数

深拷贝与浅拷贝

浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作

如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的重复释放内存空间的问题

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
class Person {
public:
Person() {
cout << "无参构造函数!" << endl;
}

Person(int age, int height) {
cout << "有参构造函数!" << endl;
m_age = age;
m_height = new int(height); // 堆区开辟
}

//拷贝构造函数
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age; // 非堆区开辟
m_height = new int(*p.m_height); // 堆区开辟

}

//析构函数
~Person() {
cout << "析构函数!" << endl;
if (m_height != NULL)
{
delete m_height;
}
}
public:
int m_age;
int* m_height; // 堆区开辟
};

列表初始化

一种简化语法的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
public:

////传统方式初始化
//Person(int a, int b, int c) {
// m_A = a;
// m_B = b;
// m_C = c;
//}

//初始化列表方式初始化
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
private:
int m_A;
int m_B;
int m_C;
};

静态成员变量

即可以通过对象名访问,也可以通过类名访问(类名::静态成员变量名)
静态成员变量特点

  1. 在编译阶段分配内存
  2. 类内声明,类外初始化
  3. 所有对象共享同一份数据

定义与初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person
{
public:
static int m_A; //静态成员变量
private:
static int m_B; // 如果设为私有,类外也无法直接访问
};
int Person::m_A = 10; // 初始化
int Person::m_B = 10;

Person p1;
p1.m_A; // 创建对象,利用对象名调用
Person::m_A; // 直接通过类名调用

this指针(重要)

this指针在Java中也存在,不过用法上还是略有区别

this指针指向被调用的成员函数所属的对象

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person
{
public:
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}

Person& PersonAddPerson(Person p)
{
this->age += p.age;
//返回对象本身
return *this;
}

int age;
};

// 因返回的是对象本身,所以可以连续调用
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);

友元

友元分为三种:

  • 全局函数作友元
  • 类作友元
  • 成员函数作友元

第一种最简单,直接声明前加上friend关键字,放在类的最前头即可

1
friend void goodGay(Building * building);

第二种需要考虑到类定义的顺序
被友元访问的那个类,需要事先声明,顺序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class B;

class A
{
public:
...
private:
B *b;
};

class B
{
friend class A;
};

第三种成员函数作友元,与类友元类似,不过只有类中特定的一个成员函数可以访问。总体形式一样,区别在于声明语句

1
friend void goodGay::visit();

运算符重载

不能重载的运算符有5种,分别是.->sizeof?:::

加号

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 Person {
public:
Person() {};
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//成员函数实现 + 号运算符重载
Person operator+(const Person& p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}

public:
int m_A;
int m_B;
};

//全局函数运算符重载 可以发生函数重载
Person operator+(const Person& p2, int val)
{
Person temp;
temp.m_A = p2.m_A + val;
temp.m_B = p2.m_B + val;
return temp;
}

使用方法

1
2
3
4
Person p1(10, 10);
Person p2(20, 20);
Person p3 = p2 + p1;
Person p4 = p3 + 10;

左移

只能在全局函数中实现

1
2
3
4
5
6
//全局函数实现左移重载
//ostream对象只能有一个
ostream& operator<<(ostream& out, Person& p) {
out << "a:" << p.m_A << " b:" << p.m_B;
return out;
}

并且要作为友元,放在原类中

1
friend ostream& operator<<(ostream& out, Person& p);

继承

基本语法

class A : public B;

A 类称为子类 或 派生类
B 类称为父类 或 基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base1
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};

//公共继承
class Son1 :public Base1
{
public:
void func()
{
m_A; //可访问 public权限
m_B; //可访问 protected权限
//m_C; //不可访问
}
};

成员函数变量同名

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
1
2
3
4
s.m_A; // 子类的变量
s.Base::m_A; // 父类的变量,加类名访问
s.func(); // 子类的函数
s.Base::func(); // 父类的函数,加类名访问

多态

父类指针或引用指向子类对象

  • 静态多态: 函数重载和运算符重载属于静态多态,复用函数名——编译阶段确定函数地址
  • 动态多态: 派生类和虚函数实现运行时多态——运行阶段确定函数地址

父类定义一个虚函数,子类通过重写虚函数实现多态

1
2
3
4
// 父类
virtual void speak() {
...
}

计算器

创建一个计算器父类与加法子类,并重写虚函数

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
//多态实现
//抽象计算器类
//多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
class AbstractCalculator
{
public :

virtual int getResult()
{
return 0;
}

int m_Num1;
int m_Num2;
};

//加法计算器
class AddCalculator :public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 + m_Num2;
}
};

使用方法:

1
2
3
4
5
6
//创建加法计算器
AbstractCalculator *abc = new AddCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc; //用完了记得销毁

纯虚函数与抽象类

这个点也是比较陌生的点,平常很少用到不涉及,且和Java有一定程度上的区别

考虑到父类一般是抽象类,所编写的纯虚函数可能并不存在对应的实现,因此就要把它定义为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;

当类中有了纯虚函数,这个类也称为==抽象类==

抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
  • 类中只要有一个纯虚函数就称为抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base
{
public:
virtual void func() = 0;
};

class Son :public Base
{
public:
void func()
{
cout << "func调用" << endl;
};
};

虚析构与纯虚析构

存在的问题:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象(作用),提醒子类必须重写自己的析构函数
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象
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
class Animal {
public:

Animal()
{
cout << "Animal 构造函数调用!" << endl;
}
virtual void Speak() = 0;

virtual ~Animal() = 0;
};

Animal::~Animal()
{
cout << "Animal 纯虚析构函数调用!" << endl;
}

class Cat : public Animal {
public:
Cat(string name)
{
cout << "Cat构造函数调用!" << endl;
m_Name = new string(name);
}
void Speak()
{
cout << *m_Name << "小猫在说话!" << endl;
}
~Cat()
{
cout << "Cat析构函数调用!" << endl;
if (this->m_Name != NULL) {
delete m_Name;
m_Name = NULL;
}
}
public:
string* m_Name;
};

未完待续……

参考资料

  1. 黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难
  2. 《C++ Primer》