C++学习随手记

C++入门基础

重载

同一作用域下的同名函数,但是形参必须不同.
原理是在编译过程中,C++编译器根据函数的名称和形参分配地址.

引用

&变量
相当于是给变量取了个别名
主要使用的话是作为形参,作用一般有两个:1.避免值拷贝,浪费内存(递归中) 2.做到直接修改形参中的变量.
可以结合const,只做到第一点

this指针

this指针的由来,先涉及到一个类,是如何在内存中存储的.类的不同实例中,类成员变量分开存储,但是类成员函数是一样的东西,所以是放在公共代码区的.
那么,在实例使用函数的时候,函数怎么知道是哪一个实例在使用呢?使用哪个实例的成员变量呢?
this指针因此而生,实例在使用成员变量的时候,传参的时候,传的第一个就是this指针,也就是实例的地址
在这里插入图片描述
每个成员函数的第一个形参实际上是一个隐含的this指针,该指针用于接收调用函数的对象的地址,用this指针就可以很好地访问到该对象中的成员变量

const

const的应用场景

  • 修饰对象
class MyClass {
public:
    int val;
};
const MyClass obj; // obj 是 const 对象
obj.val = 10;      // 编译错误:无法修改 const 对象的成员

const修饰的对象obj,说明obj无法修改成员变量,除非成员变量声明为mutable

  • 修饰成员函数
class MyClass {
public:
    void nonConstFunc() { val = 10; }       // 非 const 函数,可修改成员
    void constFunc() const { /* 不能修改 val */ } // const 函数,禁止修改成员
    int val;
};

void constFunc() const const修饰的是成员函数,也就是说该函数不能修改成员变量的状态.

  • 修饰指针/引用
    const int* ptr:指针指向的内容不可变,指针本身可变.
    int* const ptr:指针本身不可变,指向内容可变.
    cosnt int& ref:引用的对象不可变.

const的四个主要面试题

const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。

const 对象可以调用非 const 成员函数吗?

不可以
const对象的状态必须保持不变,但是非const成员函数可能改变成员变量,因此不行

class MyClass {
public:
    void nonConstFunc() { val = 10; } // 非 const 函数
    int val;
};

int main() {
    const MyClass obj; // obj 是 const 对象
    obj.nonConstFunc(); // 编译错误!const对象不能调用非const函数
    return 0;
}
非 const 对象可以调用 const 成员函数吗?

可以,const成员函数不改变对象中成员变量的内容,刚好非const对象无要求

class MyClass {
public:
    void constFunc() const { /* 不修改状态 */ } // const 函数
    int val;
};

int main() {
    MyClass obj; // 非 const 对象
    obj.constFunc(); // 合法,调用 const 函数
    return 0;
}
const 成员函数内可以调用其他非 const 成员函数吗?

不可以,const函数不可以改变对象状态,但是非const成员函数可能修改状态,因此会破坏const承诺.

class MyClass {
public:
    void nonConstFunc() { val = 10; } // 非 const 函数
    void constFunc() const {
        nonConstFunc(); // 编译错误!const函数不能调用非const函数
    }
    int val;
};
非 const 成员函数内可以调用其他 const 成员函数吗?

可以,非const成员函数没有要求不改变对象状态,因此可以调用const或者非const成员函数

class MyClass {
public:
    void constFunc() const { /* 不修改状态 */ } // const 函数
    void nonConstFunc() {
        constFunc(); // 合法,非const函数可以调用const函数
    }
};

static成员

static修饰的成员变量,叫 静态成员变量
static修饰的成员函数,叫 静态成员函数
静态成员变量一定要在类外进行初始化

特性

  • static成员为所有类对象所共享,不属于某个具体的对象
#include <iostream>
using namespace std;
class Test
{
private:
	static int _n;
};
int main()
{
	cout << sizeof(Test) << endl;
	return 0;
}

最后结果是1,因为静态成员_n是存储在静态区的,属于整个类.

  • 静态成员必须在类外定义,定义时不添加static关键字
class Test
{
private:
	static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;

**注意:**这里的_n虽然时private,但是我们在类外进行了访问,这是一个特例

  • 静态成员函数没有this指针,不能访问任何非静态成员
class Test
{
public:
	static void Fun()
	{
		cout << _a << endl; //error不能访问非静态成员
		cout << _n << endl; //correct
	}
private:
	int _a; //非静态成员
	static int _n; //静态成员
};

友元

友元提供了一种突破封装的方式,有时提供了遍历,但是友元会增加耦合度,破坏了封装,因此友元不宜多用

友元函数

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中非公有成员。

内存管理

C++中内存的区域分布从上往下依次为:

  • 内核空间(用户代码不能读写)
  • 栈(向下增长)
    存放的是非静态局部变量/函数参数/返回值
  • 内存映射段()
    是指将文件、设备或其他外部数据源直接映射到进程虚拟地址空间的一段内存区域。通过这种映射,进程可以像访问普通内存一样直接读写数据,而无需使用传统的 I/O 系统调用(如 read/write)。内存映射段的核心作用是实现高效的数据交互和资源共享
  • 堆(向上增长)
    存放的是运行时的动态内存分配
  • 数据段(全局数据,静态数据)
    又叫静态区,存储全局数据和静态数据
  • 代码段(可执行代码,只读常量)
    又叫常量区,存放可执行代码和只读常量

new和delete

没啥好说的,提一嘴和C中的malloc和free的区别就是,new和delete会自动调用构造函数和析构函数.(除了开辟空间的共同作用以外)

模板

编写与类型无关的通用代码,是代码复用的一种手段.
模板是泛型编程的基础.

函数模板

template<typename T1,typename T2,,typename Tn>
返回类型 函数名(参数列表)
{
  //函数体
}

举例使用:

#include <iostream>
#include <vector>
using namespace std;

// 函数模板:打印容器元素(依赖类型需用 typename)
template <typename Container>
void print_container(const Container& cont) {
    typename Container::const_iterator it; // Container::const_iterator 是依赖类型
    for (it = cont.begin(); it != cont.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
}

int main() {
    vector<int> vec = {1, 2, 3};
    print_container(vec); // 推导 Container 为 vector<int>,输出:1 2 3
    vector<string> vec1 = {a,b,c};
    print_container(vec1);
    return 0;
}

类模板

继承

继承方式

C++中有三种继承方式:
public继承,protected继承,private继承
类中的成员变量和函数有三种访问限定符
public成员,protected成员,private成员

经过继承后,父类中的成员会变成什么样子?见此处

重点:这里提一嘴,父类中的private成员,不论继承方式,在子类中都是不可见的(不可见的指的是子类对象在类外或类以内都无法访问).
如果想要可见,那么得定义成protected成员.

一般都是用public继承,

  • 默认继承方式
    class默认为private继承(不写清的话)
    struct默认为public继承

基类和派生类对象赋值转换

例如父类:Person,子类:Student
子类可以给父类对象赋值
例如:

Student s;
Person p = s;//子类对象赋值给父类
Person* p = &s; //子类对象地址赋值给父类对象指针
Person& p = s;//子类对象赋值给基类引用

形象点的说法叫切割,也就是把子类中的和父类的交集切割过去

继承中的作用域

子类继承父类以后,二者的作用域是相同的,如果子类和父类中有同名成员,子类将访问的是子类的成员,屏蔽父类的成员
如果想访问父类的成员,得限定作用域

void fun(){
	cout<< Person::_num<<endl;//指定访问父类中的成员函数
}

这个访问包括成员函数,父子类中的成员函数相同并不构成重载,因为重载需要在同一作用域中

派生类中的六大默认成员函数的不同之处

挖个坑

继承与友元

友元关系不能继承,也就是基类的友元可以访问基类的私有和保护成员的,但是不能访问派生类的私有和保护成员

如果想让友元函数也想访问派生类中的成员,那么需要在派生类中也进行友元的声明

继承与静态成员

如果基类中定义了一个static成员变量,那么整个继承体系中只有这一个static成员实例。
举个例子:
person, Student,Teacher,如果在person中设置了一个static count,然后在构造函数中设置自增,那么随时可以通过这个count获得person,Student和Teacher的总数

继承的方式

  • 单继承
    一个子类只有一个直接父类

  • 多继承
    一个子类有两个或两个以上的直接父类称为多继承

  • 菱形继承
    继承形状形似一个菱形
    在这里插入图片描述
    这里能看出来菱形继承的继承方式存在数据冗余和二义性的问题
    举例:Assistant同时继承了Student和Teacher,这二者又继承自Person

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
	string _name; //姓名
};

class Student : public Person
{
protected:
	int _num; //学号
};

class Teacher : public Person
{
protected:
	int _id; //职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; //主修课程
};
int main()
{
	Assistant a;
	a._name = "peter"; //二义性:无法明确知道要访问哪一个_name
	return 0;
}

也就是,Person中的成员name,同时被Teacher和Student继承,那么,访问Asistant中的name,则会搞不清楚是Teacher的还是student。
针对以上问题,可以显式指定访问哪个成员

a.student::_name = "张同学"
a.teacher::_name = "张老师"

但是这样还是存在数据冗余的问题,_name还是存储了两份

这里
在这里插入图片描述

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
	string _name; //姓名
};

class Student : virtual public Person //虚拟继承
{
protected:
	int _num; //学号
};

class Teacher : virtual public Person //虚拟继承
{
protected:
	int _id; //职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; //主修课程
};
int main()
{
	Assistant a;
	a._name = "peter"; //无二义性
	return 0;
}

虚拟继承的原理:

  • 测试代码
#include <iostream>
using namespace std;
class A
{
public:
	int _a;
};
class B : public A
{
public:
	int _b;
};
class C : public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

在这里插入图片描述
在这里插入图片描述
根据内存分布图,能看出来,出现数据冗余性的原因是,D中存在着两个_a成员变量

  • 当使用了虚拟继承以后再查看内存分布
    在这里插入图片描述
#include <iostream>
using namespace std;
class A
{
public:
	int _a;
};
class B : virtual public A
{
public:
	int _b;
};
class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

在这里插入图片描述
在这里插入图片描述
其中D类对象中的_a成员被放到了最后,原先存放_a的地方变成了两个指针,虚基表指针,他们分别指向一个虚基表
虚基表中包含两个数据:

  • 一是为多态的虚表预留的存偏移量的位置(这里不关心)
  • 二就是当前类对象位置距离公共虚基类偏移量

也就是说,这两个指针经过一系列的计算,都可以找到成员_a

  • 如果将D类赋值给B类对象,那么在这个切片过程中(前面提到),就需要虚基表中的第二个数据找到公共虚基类A的成员,得到切片后该B类对象在内存中仍然保持这种分布情况

多态

概念

多态,就是函数调用的多种形态,不同的对象在完成同一件事的时候,产生不同的动作和结果

例如,在现实生活当中,普通人买票是全价,学生买票是半价,而军人允许优先买票。不同身份的人去买票,所产生的行为是不同的,这就是所谓的多态。

多态的定义和实现

多态是指,不同继承关系的类对象,去调用同一函数,产生了不同的行为
在继承中想要构成多态需要满足两个条件:
1、必须通过基类的指针或者引用调用虚函数
2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数

被virtual修饰的类成员函数被称为虚函数

class Person
{
public:
	//被virtual修饰的类成员函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};

此处的virtual和虚继承的virtual确实是同一个关键字,但是之间没有任何关系,一个是实现多态,一个是实现虚继承,解决菱形继承的数据冗余性和二义性。

虚函数的重写

若派生类中有一个与基类完全相同的虚函数(返回值,函数名,参数列表完全相同),则称之为子类的虚函数重写了父类的虚函数
以下代码,Student和Soldier重写了父类Person的虚函数

//父类
class Person
{
public:
	//父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	//子类的虚函数重写了父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
//子类
class Soldier : public Person
{
public:
	//子类的虚函数重写了父类的虚函数
	virtual void BuyTicket()
	{
		cout << "优先-买票" << endl;
	}
};

现在就可以通过父类Person的指针或引用调用虚函数BuyTicket,此时不同类型的对象,产生的就是不同的结果

void Func(Person& p)
{
	//通过父类的引用调用虚函数
	p.BuyTicket();
}
void Func(Person* p)
{
	//通过父类的指针调用虚函数
	p->BuyTicket();
}
int main()
{
	Person p;   //普通人
	Student st; //学生
	Soldier sd; //军人

	Func(p);  //买票-全价
	Func(st); //买票-半价
	Func(sd); //优先买票

	Func(&p);  //买票-全价
	Func(&st); //买票-半价
	Func(&sd); //优先买票
	return 0;
}

规范起见,子函数中的虚函数需要写上virtual进行重写

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值