面对对象:
和Java相同,C++认为万事万物皆对象,包括重载。
不同:
1. 接口:
C++中没有接口,有抽象类,通过继承的方式,实现虚函数, 虚函数分为:纯虚函数和普通虚函数
//纯虚函数
class Man {
public :
virtual void eat() = 0;
};
//普通虚函数
class Man {
public :
vitual void eat ();
};
struct和class区别
在C++中 struct和class唯一的区别就在于 默认的访问权限不同
区别:
- struct 默认权限为公共
- class 默认权限为私有
也就是说struct也可以理解为类,可以拥有构造函数
继承时基类前的修饰符
//用于限制访问基类的权限
class Person{
}
class Man : public Person{
}
访问权限总结
-
公有继承(Public Inheritance) :
- 基类的公有成员在派生类中仍然是公有的。
- 基类的保护成员在派生类中仍然是保护的。
- 基类的私有成员在派生类中不可访问。
- 适用于表示“是一个”关系。
-
保护继承(Protected Inheritance) :
- 基类的公有成员在派生类中变为保护成员。
- 基类的保护成员在派生类中仍然是保护成员。
- 基类的私有成员在派生类中不可访问。
- 适用于不希望基类的公有接口被外部代码直接访问的情况。
-
私有继承(Private Inheritance) :
- 基类的公有成员在派生类中变为私有成员。
- 基类的保护成员在派生类中变为私有成员。
- 基类的私有成员在派生类中不可访问。
- 适用于实现“拥有”关系,而不是“是一个”关系。
析构函数
编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法: 类名(){}
引用和指针
指针:可以指向任意类型变量,可以动态变化,不过消耗资源会更多一些。
引用:语法: 数据类型 &别名 = 原名
引用注意事项:
-
初始化之后只能绑定一次
-
必须初始化 只能绑定某一个变量,作为变量的别名,绑定之后就无法改变了。Java是一个引用传递的编程语言,因此,在函数参数传递时不需要复制大对象。
-
引用可以作为函数参数
-
引用作为函数的返回值
本质:引用的本质在c++内部实现是一个指针常量
引用和指针在底层实现上有相似之处,引用通常可以被视为一种特殊类型的指针。 而且引用比指针消耗的资源更少,这也是引用存在的意义
那么可不可以给指针创建引用呢? 当然是可以的:引用是给任意类型创建一个常量指针,通过创建常量指针,接下来就是和Java中使用引用的方式相同了
创建对象:
C++:
class Man{
public :
Man(){
}
Man(const ){
}
void eat(){
cout << "Man is eating" << endl;
}
};
int main(){
//栈上创建
Man n;
n.eat();
//堆上创建
Man nn = new Man();
nn.eat();
return 0;
}
构造函数的分类及调用
Java
构造函数:
public class Man{
权限修饰符 类名 (参数){
//父类构造函数
super();
}
}
C++
两种分类方式:
- 按参数分为: 有参构造和无参构造
- 按类型分为: 普通构造和拷贝构造
三种调用方式:
- 括号法
- 显示法
- 隐式转换法
- 匿名对象的错误用法:
Person p3 = Person(p1);这行代码看似是想通过p1创建一个新的Person对象,但实际上编译器会将Person(p1)视为一个对象声明,而不是一个匿名对象的初始化。这是因为在 C++ 中,编译器在处理这种情况时会认为你在声明一个新的Person类型的对象,而不是在创建一个临时对象。
构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
深拷贝问题: 当C++类中有在堆中分配内存的属性,如果只进行浅拷贝,会导致执行完delete之后,另一个拷贝过来的对象再释放会导致堆区重复释放的问题。
向上向下类型转换
Java
class Man{
private int age;
private String name;
public Man(){
}
public void eat(){
System.out.println("man eat")
}
}
public class Client{
public static void main(String[]args){
//向上
Man man = new Man();
Object obj = (Object)man;
//向下
Object la = new Man();
Man h = (Man)la;
}
}
C++:
class Man{
public:
//1. 无参构造函数
Man(){
}
//2. 有参构造
Man(int age, std::string name) : age(age), name(name){
}
private:
int age;
std::string name;
}
typedef using #define区别:
函数:
Java函数和属性存储在一起
C++函数和属性分开存储,因此空指针的对象 也可以调用类中的方法
const修饰成员函数
类作为友元:
friend class goodGay;
全局函数作为友元函数:
friend void goodGay(Building * building);
类的成员函数作为友元函数:
friend void goodGay::visit();
C++的多态:
多态本身并不会直接导致内存释放问题,但在使用多态时,如果不正确地管理对象的生命周期,可能会引发内存泄漏或未定义行为。以下是一些常见的导致内存释放问题的情况:
-
缺少虚拟析构函数:
- 如果基类的析构函数不是虚拟的,当通过基类指针删除子类对象时,只会调用基类的析构函数,而不会调用子类的析构函数。这会导致子类中分配的资源(如动态分配的内存、打开的文件等)没有被释放,从而造成内存泄漏。
class Base { public: ~Base() { /* 基类析构逻辑 */ } }; class Derived : public Base { public: ~Derived() { /* 子类析构逻辑 */ } }; Base* obj = new Derived(); delete obj; // 只调用 Base 的析构函数,Derived 的析构函数不会被调用 -
错误的对象管理:
- 如果使用了不当的对象管理策略(如使用原始指针而不是智能指针),可能会导致对象的重复释放或未释放。例如,如果一个对象被多个指针引用,可能会在一个指针释放后,其他指针仍然指向已释放的内存,导致未定义行为。
-
不正确的拷贝和赋值:
- 如果类没有正确实现拷贝构造函数和赋值运算符,可能会导致多个对象共享同一资源,进而在析构时出现问题(如双重释放)。
-
资源管理不当:
- 在多态情况下,子类可能会分配特定的资源(如动态内存、文件句柄等),如果这些资源没有在子类的析构函数中正确释放,也会导致内存泄漏。
为了避免这些问题,建议遵循以下最佳实践:
- 在基类中使用虚拟析构函数。
- 使用智能指针(如
std::unique_ptr或std::shared_ptr)来管理动态分配的对象。 - 确保正确实现拷贝构造函数和赋值运算符,遵循“规则五”(Rule of Five)原则。
- 定期使用工具(如 Valgrind)检查内存泄漏和其他内存管理问题。
左值和右值
左值:可以取地址的对象
左值引用:
int c = 1;
int& a = c;
语法:类型& 变量名 = 左值
右值:不能取地址的对象(比如一个表达式,一个临时对象)
int&& i = x + y;
class Man{
};
int main(){
Man a = Man();
//这种创建对象的方法,其实是右边创建了匿名对象,交给右边的a,此时Man()就是一个临时对象
//Man a本身已经创建了一个对象,赋值给a,后面又将这个匿名对象拷贝到a中,这个操作浪费空间时间,因此不建议使用,不过可以用来理解什么是左值,什么是右值
return 0;
}
右值引用:
int &&a = x + y;
class Man{
}
Man &&m = Man();
语法:
类型 &&变量名 = 右值;
左右值的转化
move函数可以将左值转化为右值,
右值是为了解决函数中不能返回局部参数,可以延长对象的生命周期,如果出现从函数中返回局部对象的情况,将其转化为右值返回,此时会将右值存储在一个特殊的地方,
在C++中类中所有函数都可以只声明不写函数体?
可以,编译的时候没问题,但是在调用的时候就会报错