C++知识随笔

关键字与运算符

指针与引用

指针

1)本身是一个变量,用于存放某个变量的地址,指向内存的某个存储单元
2)本身有地址,所以有指向指针的指针。

引用

实质上是变量的别名

两者的比较
(2)引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。

(3)可以有const指针,但是没有const引用;

(4)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

(5)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

(6)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

(7)”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;

(8)指针和引用的自增(++)运算意义不一样;

(9)如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;

原文链接

const关键字

const的作用是让被它修饰的值不能修改,是只读变量,且必须在定义的时候就给它赋初值。

常量指针

先写常量再写指针,形式如下,,这个指针指向一个常量,不能通过常量指针改变这个对象的值。常量指针强调的是对其所指对象的不可改变性。

int temp = 10;

//形式一
//const 数据类型 *指针变量 = 变量名
const int *a = &temp;

//形式二
//数据类型 const *指针变量 = 变量名
int const *a = &temp;

//更改
*a = 9//会报错,只读对象不可修改
temp = 9;
------------------------
printf("%d\n",temp);
printf("%d\n",*a);	
temp = 9;
printf("%d\n",*a);
10
10
9

指针常量

先写指针再写常量,形式如下,指针值是一个常量,只能在定义时初始化,不能在其他地方修改。指针常量强调的是指针的不可改变性。

//形式
// 数据类型* const 指针变量 = 变量名
int temp = 10;
int temp1 = 11;
int* const p = &temp;

//更改
p = &temp1;//会报错,只读对象不可修改
*p = 9;
------------------------
printf("%d\n",temp);
printf("%d\n",*p);	
*p = 9;
printf("%d\n",*p);
10
10
9

常量指针常量

指针值和指向值都是常量

//形式
//const 数据类型* const 指针常量 = 变量名
int a = 1;
int b = 2;
const int* const p = &a;
*p = 6//错误
p = &b//错误

define、typedef和inline

define

只是简单的字符串替换,没有类型检查,不安全
在编译的预处理阶段起作用
可以用来防止头文件的重复引用
不分配内存,给出的是立即数,有多少次使用就进行多少次替换

typedef

有对应的数据类型,需要进行判断
在编译、运行时起作用
在静态存储区中分配空间,在程序运行时,只存在一个内存拷贝

inline

先将内联函数编译完成,生成的函数体直接插入被调用的地方,减少了压栈、跳转和返回操作,没有普通函数调用时的额外开销
会进行类型检查
是对编译器的一种请求,可能会被编译器拒绝

C++中对inline编译的限制

不能存在任何形式的循环语句
不能存在过多的条件判断语句
函数体不能过于庞大
内联函数声明必须存在于调用语句之前

override和overload

override(重写)

是重写(覆盖)了一个方法,以实现不同的功能,一般是子类继承父类的时候,重写父类的方法。
规则
1.重写方法的参数列表,返回值,所抛出的异常与被重写方法一致
2.被重写方法不能为private
3.静态方法不能被重写为非静态方法
4.重写方法的访问修饰符必须大于被重写方法的静态修饰符(public>protect>default>private)

overload(重载)

是重载方法,一个方法的名称相同,而参数形式不同。即在同一个类中,有不同版本的同名方法。
规则
1.不能通过访问权限、返回值、所抛出的异常进行重载
2.不同的参数类型可以是不同的参数类型、不同的参数个数、不同的参数顺序(参数类型必须不一样)
3.方法的异常类型和数目不会对重载造成影响

小结
方法的重写以及重载是多态的实现方式
使用多态是为了避免在父类里大量重载,引起代码臃肿且维护困难
重写与重载的区别在于,使用override修饰的方法,始终只有一个被使用的方法

new和malloc

new

1.new内存失败时,会抛出bac_alloc异常,不会返回NULL。new内存成功后,返回的是对象类型的指针,无需进行类型转换,是符合类型安全性的操作符
2.new内存无需指定内存块大小
3.opeartor new/ opeartor delete可以被重载
4.new/delete 会调用对象的构造/析构函数以完成对象的构造/析构
5.new/delete的本质是C++标准运算符
6.new从自由存储区上获取空间

malloc

1.malloc内存失败时,不会抛出异常,会返回NULL。malloc内存成功后,返回的是void *,需要强制转换成我们所需要的类型,不腹黑类型安全性
2.malloc内存时需要显式的声明内存大小
3.malloc/free不允许被重载
4.malloc/free不会调用对象的构造/析构函数
5.malloc/free本质上是C语言的标准函数库
6.malloc从堆上动态内存分配空间

C++标准并没有给出new/delete应该如何实现,但很多编译器的new/delete都是以malloc/free为基础来实现的

C++三大特性

封装

封装指的是将数据与行为包装在一个整体中,并通过访问权限控制成员变量和成员函数的访问权限。

封装的类有如下的访问类型:
1)公有( public )成员可以在类外访问;
2)私有( private )成员只能被该类的成员函数访问;
3)保护( protected )成员只能被该类的成员函数或派生类的成员函数访问。

在类的内部,无论成员被声明为public、protect还是private,都是可以相互访问的,没有访问权限的限制。
在类的外部,只能通过对象访问成员,并且通过对象只能访问public属性的成员,不能访问protect和private的成员。

数据成员通常是私有的,成员函数通常有一部分是公有的,一部分是私有的。因为类的公有的函数可以在类外被访问,也称之为类的接口。(实际中具体的访问权限情况根据实际情况而定)

封装特点: 程序更模块化,更易读易写,提升了代码重用到一个更高的层次。

继承

继承允许我们依据另一个类来定义一个类,已有的类被称为基类,新建的类被称为派生类。派生类直接获得的基类的属性和方法,使得创建和维护一个应用程序变得更容易

继承基类成员访问权限的变化:

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
2.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
3.在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
原文链接

多态

C++中的多态性是指通过同一函数接口调用不同对象的方法,实现不同的行为。分为运行时多态编译时多态

运行时多态

运行时多态,又称为动态多态,因为它是在程序运行时动态实现的。
C++通过虚函数来实现运行时多态。允许在基类中写入虚函数,拥有虚函数的类被称为抽象类。根据不同派生类的需要重写不同的虚函数,以适应多种不同需求。

编译时多态

编译时多态,,又称静态多态。
C++通过函数重载、运算符重载以及模板实现编译时多态。
函数重载指的是在同一个作用域中定义多个函数,它们的函数名相同,但参数列表不同。当调用重载函数时,编译器将根据函数调用的参数类型和个数来选择最合适的函数版本,从而实现多态性。
模板是一种通用的函数或类,可以根据所传递的参数类型和数量来生成特定的函数或类,从而实现多态性。

虚函数

当基类希望派生类定义适合自己的版本,就将这些函数声明成虚函数(virtual)

#include <iostream>
#include <stdio.h>
using namespace std;

class Base
{
	public:
	virtual void func(){
		printf("Base Class func\n");
	}

};

class Drived :public Base{
	public:
	void func(){
		printf("Drived Class func\n");
	}
};



int main()
{
	Base* base = new Base;
	Drived* drived = new Drived;
	base ->func();
	drived ->func();
	
	delete base ;
	delete drived ;
	-----------------------------------------
/*
输出结果
Base Class func
Drived Class func
*/
}

虚函数的工作方式

虚函数依赖虚函数表工作,虚函数表用来保存虚函数地址,当我们用基类指针指向派生类时,虚表指针vptr指向派生类的虚函数表。

虚函数是动态绑定的

也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数,这是虚函数的基本功能。

多态(不同继承关系的类对象,调用同一函数产生不同行为)

1、调用函数的对象必须是指针或者引用
2、被调用的函数必须是虚函数(virtual),且完成了虚函数的重写(派生类中有一个跟基类完全相同的虚函数)

动态绑定绑定的是动态类型

所对应的函数或属性依赖于对象的动态类型,发生在运行期

构造函数不能是虚函数

在构造函数中调用虚函数,实际执行的是负类的对应函数,因为自己还没有构造好

析构函数可以是虚函数

在一个复杂的类结构中,往往也是必须的

析构函数可以是纯虚函数

但纯虚析构必须要有定义体,因为析构函数在子类中是隐式调用

inline、static不能带有virtual关键字

inline

表面上,inline在编译时展开,必须要有实体。内联函数在编译期间用被调用的函数体代替函数的调用指令,而虚函数的多态特性需要在运行时根据对象类型才知道调用哪个虚函数,所以没法在编译时进行内联函数展开。

但是需要注意,inline只是一个建议性关键字,关键取决于编译器,不会强制性执行。两者关键字存在的时候,如果是多态调用,编译器会自动忽略inline这个建议,因为没法将这个虚函数直接展开,这个建议无了。不是多态就可以利用此建议。

static

static属于class自己的类相关,必须要有实体。static成员没有this指针,直接利用类域指定的方式调用。
virtual函数一定要通过对象来调用,有隐藏的this指针。虚函数都是为多态服务的。多态是运行时决议,而静态成员函数都是编译性决议。

派生类重写虚函数定义时必须和父类完全一致

除了一个特例,如果父类中返回值是一个指针或者引用,子类重写是可以返回这个指针或引用的派生。

class A
{
public:
	virtual A& test()
	{
		cout << "A::test" << endl;
		return *this;
	}
};
 
class B : public A
{
public:
	B& test()  // 子类返回子类引用(引用对引用, 指针对指针)
	{
		cout << "B::test" << endl;
		return *this;
	}
};
 
int main()
{
	B b;
	A& a = b;  // A* a = b
	a.test();  // a->test()
	return 0;
}
--------------------------------
/*
输出结果
B::test
*/

小结

1.函数声明虚函数后,会在对应域中留下个指针,指向一个虚函数表。
2.派生类继承后,会在对应的基类域中留下虚函数表指针,如果完成虚函数重写,就会把原来虚函数表内的指针给覆盖掉。
3.虚函数表实际上就是一个指针数组。存储的指针就是指向函数实现的地址(这里稍微有点复杂的指向,要经过多次变换),一般此数组最后放了一个nullptr。
4.虚函数和普通函数一样,存在代码段的,对象里存的是指向虚函数表的指针,虚表里存的是虚函数的指针,而虚表在vs下是存在代码段的。
5.多态调用时,根据切片(此时对象为基类指针或者引用)调用对应派生类中基类域的虚函数,根据虚函数表找到对应虚函数指针进行调用虚函数,完成多态。
6.派生类自己新增加的虚函数按其在派生类的声明次序增加到虚表的后面。

纯虚函数

纯虚函数相当于只是声明一下。
纯虚函数表示当前类为抽象类,不可被实例化。被继承的派生类如果不重写此纯虚函数,那么也还是抽象类。只有被重写后,才不是抽象类。

#include <iostream>
#include <stdio.h>
using namespace std;

class Base
{
	public:
	virtual void func()=0;//纯虚函数的声明


};

class Drived :public Base{
	public:
	void func(){
		printf("Drived Class func\n");
	}
};



int main()
{
	Drived* drived = new Drived;
	drived ->func();
	
	delete drived ;
	
}
---------------------
/*
输出结果
Drived Class func
*/

虚继承

为什么要进行虚继承
1、为了解决多继承时的命名冲突和冗余数据问题
C++提出了虚继承,使得在派生类中只保留一份间接基类的成员。其中多继承是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。
2、虚继承的目的是让某个类做出声明,承诺愿意共享它的基类
其中,这个被共享的基类被称为虚基类。在这种机制下,不论虚基类在继承体系中出现多少次,在派生类中都只包含一份虚基类的成员。

#include<iostream>
#include<string>
using namespace std;
class Base
{
public:
	int m_a;
};

class Son1 :public Base
{
};

class Son2 :public Base
{
};

class Grandson :public Son1, public Son2
{
};

int main()
{
	Grandson gs;
	gs.Son1::m_a = 20;
	gs.Son2::m_a = 40;
	cout << "Son1_gs的年龄为" << gs.Son1::m_a << endl;//Son1_gs的年龄为20
	cout << "Son2_gs的年龄为" << gs.Son2::m_a << endl;//Son2_gs的年龄为40
	cout << "Son2_gs的年龄为" << gs.m_a << endl;//error: request for member ‘m_a’ is ambiguous
	return 0;
}

此时gs.m_a这个成员意义不明,不知道继承哪个类,所以需要用到虚继承,代码如下

#include<iostream>
#include<string>
using namespace std;
class Base
{
public:
	int m_a;
};

class Son1 :virtual public Base
{
};

class Son2 :virtual public Base
{
};

class Grandson :public Son1, public Son2
{
};

int main()
{
	Grandson gs;
	gs.Son1::m_a = 20;
	gs.Son2::m_a = 40;
	gs.m_a = 60;
	Son1 s1;   //定义一个Son1类,并给值33
	s1.m_a = 33;
	cout << "Son1_gs的年龄为:" << gs.Son1::m_a << endl;
	cout << "Son2_gs的年龄为:" << gs.Son2::m_a << endl;
	cout << "Grandson_gs的年龄为:" << gs.m_a << endl;
	cout << "Son1的年龄为:" << s1.m_a << endl;
	--------------------------------------------------------
/*
输出结果
son1_gs的年龄为:60
Son2_gs的年龄为:60
Grandson_gs的年龄为:60
Son1的年龄为:33
*/
}

上述代码中简单实现了一个虚继承的案例,Son1和Son2有着共同的虚基类Base,Grandson又同时继承了Son1和Son2。
C++标准库中的iostream类是一个虚继承的应用案例。iostream从istream和ostream直接继承而来,而istream和ostream又都继承自base_ios的类,是典型的菱形继承。

虚继承
虚继承
base_ios
istream
ostream
iostream

虚拟继承和普通继承的区别

1)在普通继承中,如果继承的子类本身有虚函数,就会在从父类继承过来的虚表上进行扩展;而在虚继承中,如果子类本身有虚函数,编译器就会为其单独生成一个虚表指针(vptr)和虚函数表,如果子类本身没有新增虚函数,那么vptr就不会存在,也不会有对应的虚函数表。

2)普通继承中,是先按父类的方式构造对象,然后在此基础上进行扩展和覆盖;而在虚继承中,不是这样的。

3)虚继承中,父类对象的虚表是单独保存的,通过新增的虚基类指针和虚基类表,来标明各个父类对象内存空间的偏移值。

空类

空类的大小不是0

为了确保两个不同对象的地址不同,必须如此。类的实例化是在内存中分配一块地址,每个实例在内存中都有独一无二的地址。同样空类也会实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化以后就会有一个独一无二的地址了。

#include <iostream>
using namespace std;

class A{};
int main()
{
    cout << "sizeof class A is "<<sizeof(A);
    return 0;
}
-----------------------------------
/*
输出结果
sizeof class A is 1
*/

抽象类和接口的实现

接口描述了类的行为和功能,而不需要完成类的特定实现,c++中的接口是使用抽象类来实现的
1、类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用“=0”来指定的。
2、设计抽象类的目的是为了给其他类提供一个可以继承的适当的基类。抽象类不能够被用于实例化对象,它只能作为接口使用。

智能指针

智能指针是为了解决动态分配内存导致内存泄漏和多次释放同一内存所提出的,C11标准中放在<memory.h>文件中。包括:共享指针、独占指针、弱指针

共享指针(shared_ptr)

共享指针的实现机制是在拷贝构造时使用同一份引用计数

独占指针(unique_ptr)

弱指针(weak_ptr)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值