C++

指针

1.悬空指针

定义:指针被释放后,还指向原来的内存空间。如:

1
2
3
4
void *p = malloc(size);
assert(p);
free(p);
//此时p为悬空指针

避免方法:

1
2
3
4
void *p = malloc(size);
assert(p);
free(p);
p = NULL;//赋值为NULL来避免悬空指针

2.野指针

定义:指针不确定其具体指向的内存空间。如:

1
2
void *p;
//此时p为野指针

危害:可能指向任意内存段,因此它可能会损坏正常的数据,也有可能引发其他未知错误。

避免方法:

1
void *p = NULL;

重载

运算符重载

1
2
3
4
函数类型 operator运算符(形参列表)
{
重载语句;
}

operator为关键字,专门用于定义运算符重载的函数。可以将 operator运算符 看成函数名称。

运算符重载规则

1.并不是所有的运算符都能被重载,如长度运算符sizeof、条件运算符: ?(三元运算符)、成员选择符.和域解析运算符::不能被重载。

2.重载不能改变运算符的优先级和结合性。

3.重载不改变运算符的用法(即不改变原先的使用规则)。

4.运算符重载函数不能有默认参数,因为这会改变运算符操作数个数。

5.运算符重载函数既可以作为类的成员函数,也可以作为全局函数

将运算符重载函数作为类的成员函数时,二元运算符的参数只有一个,一元运算符不需要参数。之所以少一个参数,是因为这个参数是隐含的。当类的对象调用重载后的运算符时,此对象就隐形作为一个参数了。

将运算符重载函数作为全局函数时,二元操作符就需要两个参数,一元操作符需要一个参数,而且其中必须有一个参数是对象,好让编译器区分这是程序员自定义的运算符,防止程序员修改用于内置类型的运算符的性质。

6.箭头运算符->、下标运算符[ ]、函数调用运算符( )、赋值运算符=只能以成员函数的形式重载。

流操作符重载

输入流操作符>>和输出流操作符<<

coutostream类对象,ciniostream类对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//输入流操作符>>重载
istream & operator>>(istream &in, 类型 &A)
{
in >> A.成员1 >> A.成员2;
return in;
}
//返回 istream 类对象的引用,是为了能够连续读取复数,让代码书写更加漂亮

//输出流操作符<<重载
ostream & operator<<(ostream &out, 类型 &A)
{
out << A.成员1 << A.成员2;
return out;
}

下标运算符[]重载

只能以成员函数的形式进行重载,声明格式一般为:

1
2
3
4
//第一种方式:不仅可以访问元素,还可以修改元素。
返回值类型 & operator[](形参);
//第二种方式:只能访问而不能修改元素
const 返回值类型 & operator[](形参) const;

可以通过重载[]来实现变长数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Array{
public:
Array(int length = 0);
~Array();
int & operator[](int i);
const int & operator[](int i) const;
private:
int m_length; //数组长度
int *m_p; //指向数组内存的指针
};
Array::~Array(){
delete[] m_p;
}
int& Array::operator[](int i){
return m_p[i];
}
const int & Array::operator[](int i) const{
return m_p[i];
}

操作符重载

(1)流操作符重载

必须作为友元函数(关键字:friend)或普通全局函数来重载。

1
2
3
4
5
6
7
8
9
std:ostream& operator<<(std::ostream& os,const className& object){
os << object的内容;
return os;
}

std:istream& operator>>(std::istream& is,className& object){
os >> object的内容;
return is;
}

(2)一元运算符重载(++、–、-、!)

前缀形式重载调用 operator ++ () ,后缀形式重载调用 operator ++ (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
// 重载前缀递增运算符( ++ )
Time operator++ ()
{
++minutes; // 对象加 1
if(minutes >= 60)
{
++hours;
minutes -= 60;
}
return Time(hours, minutes);
}
// 重载后缀递增运算符( ++ )
Time operator++( int )
{
// 保存原始值
Time T(hours, minutes);
// 对象加 1
++minutes;
if(minutes >= 60)
{
++hours;
minutes -= 60;
}
// 返回旧的原始值
return T;
}

(3)二元运算符重载(+、-、*、/)

类成员函数或全局函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 类成员函数重载 + 运算符,用于把两个 Box 对象相加
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
//其他运算符类似
//全局函数重载
Box operator+(const Box& a, const Box& b)
{
Box box;
box.length = a.length + b.length;
box.breadth = a.breadth + b.breadth;
box.height = a.height + b.height;
return box;
}

(4)赋值运算符

  • 参数类型:引用传参,用const修饰,即const 类&
    引用传参可以提高传参效率。
1
2
3
4
5
void operator=(const Distance &D )
{
feet = D.feet;
inches = D.inches;
}
  • 返回值类型:引用返回,即 类&
    引用返回可以提高返回的效率,有返回值目的是为了支持连续赋值功能。

    1
    2
    3
    4
    5
    6
    7
    Data operator=(const Data& d)//可以提高效率
    {
    _year = d._year;
    _month = d._month;
    _day = d._day;
    return *this;
    }
  • 要检查是否给自己赋值

1
2
3
4
5
6
7
8
9
10
Data operator=(const Data& d)
{
if (this != &d)//如果两个对象的地址不相同那么就可以进行赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
}
  • 返回*this:要符合连续赋值的含义。

    1
    2
    3
    4
    5
    6
    7
    Data& operator=(const Data& d)//用引用返回,可以提高效率,减少拷贝
    {
    _year = d._year;
    _month = d._month;
    _day = d._day;
    return *this;
    }

(5)函数调用运算符()重载

函数调用运算符 () 可以被重载用于类的对象。当重载 () 时,您不是创造了一种新的调用函数的方式,相反地,这是创建一个可以传递任意数目参数的运算符函数。

1
2
3
4
5
6
7
8
9
// 重载函数调用运算符
Distance operator()(int a, int b, int c)
{
Distance D;
// 进行随机计算
D.feet = a + c + 10;
D.inches = b + c + 100 ;
return D;
}

(6)下标运算符[]重载

下标操作符 [] 通常用于访问数组元素。重载该运算符用于增强操作 C++ 数组的功能。

1
2
3
4
5
6
7
8
9
10
int& operator[](int i)
{
if( i >= SIZE )
{
cout << "索引超过最大值" <<endl;
// 返回第一个元素
return arr[0];
}
return arr[i];
}

(7)类成员访问运算符->重载

运算符 -> 通常与指针引用运算符 * 结合使用,用于实现”智能指针”的功能。

语句 p->m 被解释为 (p.operator->())->m

1
2
3
4
5
6
7
8
9
Obj* operator->() const 
{
if(!oc.a[index])
{
cout << "Zero value";
return (Obj*)0;
}
return oc.a[index];
}

引用

引用必须在声明时初始化,初始化后无法改变指向。

左值、右值的区别:

左值:可以取地址的对象

右值:不可以取地址的对象(如常量、表达式、函数返回值)

左值引用

就是对左值进行引用

1
2
3
4
5
6
7
8
9
10
11
12
// 1.左值引用只能引用左值
int t = 8;
int& rt1 = t;
//int& rt2 = 8; // 编译报错,因为8是右值,不能直接引用右值

// 2.但是const左值引用既可以引用左值
const int& rt3 = t;

const int& rt4 = 8; // 也可以引用右值
const double& r1 = x + y;
const double& r2 = fmin(x, y);

右值引用

就是对右值进行引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1.右值引用只能引用右值
int&& rr1 = 10;
double&& rr2 = x + y;
const double&& rr3 = x + y;

int t = 10;
//int&& rrt = t; // 编译报错,不能直接引用左值


// 2.但是右值引用可以引用被move的左值
int&& rrt = std::move(t);
int*&& rr4 = std::move(p);
int&& rr5 = std::move(*p);
const int&& rr6 = std::move(b);

//std::move移动语义:将一个对象中的资源移动到另一个对象(资源控制权的转移)

深拷贝与浅拷贝

浅拷贝是创建一个新对象,新对象和原对象共享同一个底层资源,简单的赋值拷贝。浅拷贝在拷贝后对象共享同一份底层资源,可以提高效率,但是当对象析构时可能会出现不确定的行为,因为资源会被重复释放

深拷贝则是创建一个新对象,,在堆中重新分配空间,新对象拥有原对象的全部资源,二者之间互不影响。深拷贝则会为每个对象创建独立的底层资源,避免了这个问题,但是会占用更多的内存

当对象中有指针指向动态分配的内存时,为了安全地复制对象,需要显式地实现深拷贝,通常通过重载类的拷贝构造函数和赋值操作符来完成

类与结构体

C语言中,结构体只是用来封装不同数据类型的数据,没有构造函数和成员函数。

C++中,结构体除了默认权限和继承默认权限不一样外,其他功能与类一样。(结构体默认public,类默认private)

在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。

继承

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

多态

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

编译时多态、静态多态(静态链接或早绑定):函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了,如函数重载运算符重载

运行时多态、动态多态(动态链接或后期绑定):根据所调用的对象类型来选择调用的函数,如派生类中的虚函数重写

虚函数与纯虚函数

1
2
3
4
5
6
//虚函数
virtual returnType functionName(){
···
}
//纯虚函数,派生类必须重写纯虚函数
virtual returnType functionName() = 0;

当类中有虚函数时,会为该类生成一个虚函数指针表(虚函数表),同时为该类添加一个虚函数表指针成员(用于访问虚函数表),表中包含一个或多个函数指针,指向该类的虚函数地址

数据抽象

只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。

优势:

  • 类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
  • 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。

数据封装

把数据和操作数据的函数捆绑在一起,通过将数据和操作数据的函数封装在一个类中来实现。

访问修饰符

  • private: 私有成员只能在类的内部访问,不能被类的外部代码直接访问。
  • public: 公有成员可以被类的外部代码直接访问。
  • protected: 受保护成员可以被类和其派生类访问。

优点:

  • 数据隐藏: 通过将数据成员声明为私有,防止外部代码直接访问这些数据。
  • 提高代码可维护性: 提供公共方法来访问和修改数据,这使得可以在不影响外部代码的情况下修改类的内部实现。
  • 增强安全性: 防止不合法的数据输入和不当的修改操作。
  • 实现抽象: 提供了一种机制,使得用户不需要了解类的内部实现细节,只需要了解如何使用类的公共接口即可。

接口(抽象类ABC)

类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的。C++的接口是通过抽象类来实现的,抽象类不能被用于实例化对象,它只能作为接口使用。

如果一个 ABC 的子类需要被实例化,则必须实现每个纯虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。可用于实例化对象的类被称为具体类

构造函数类

按参数区分:有参、无参(默认构造函数)

按照类型区分:普通、拷贝(或复制构造)

拷贝构造调用情景:

1.对象以值的形式作为函数参数

2.对象以值的形式作为函数返回值

3.将一个对象用于给另一对象进行初始化时

1
2
3
4
5
6
7
class Person{
//拷贝构造,形参必须为引用,一般会加const
//一般拷贝构造函数为浅拷贝,当成员变量中存在指针变量时需定义一个深拷贝构造函数
Person(const Person &p){
#statement
}
}

容器

vector