从Java到C++(持续更新)

68 阅读8分钟

面对对象:

和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{
}

访问权限总结

  1. 公有继承(Public Inheritance)

    • 基类的公有成员在派生类中仍然是公有的。
    • 基类的保护成员在派生类中仍然是保护的。
    • 基类的私有成员在派生类中不可访问。
    • 适用于表示“是一个”关系。
  2. 保护继承(Protected Inheritance)

    • 基类的公有成员在派生类中变为保护成员。
    • 基类的保护成员在派生类中仍然是保护成员。
    • 基类的私有成员在派生类中不可访问。
    • 适用于不希望基类的公有接口被外部代码直接访问的情况。
  3. 私有继承(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++

两种分类方式:

  • 按参数分为: 有参构造和无参构造
  • 按类型分为: 普通构造和拷贝构造

三种调用方式:

  • 括号法
  • 显示法
  • 隐式转换法
  1. 匿名对象的错误用法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区别:

image.png

函数:

Java函数和属性存储在一起

C++函数和属性分开存储,因此空指针的对象 也可以调用类中的方法

const修饰成员函数

类作为友元:

	friend class goodGay;

全局函数作为友元函数:

	friend void goodGay(Building * building);

类的成员函数作为友元函数:

	friend void goodGay::visit();

C++的多态:

多态本身并不会直接导致内存释放问题,但在使用多态时,如果不正确地管理对象的生命周期,可能会引发内存泄漏或未定义行为。以下是一些常见的导致内存释放问题的情况:

  1. 缺少虚拟析构函数

    • 如果基类的析构函数不是虚拟的,当通过基类指针删除子类对象时,只会调用基类的析构函数,而不会调用子类的析构函数。这会导致子类中分配的资源(如动态分配的内存、打开的文件等)没有被释放,从而造成内存泄漏。
    class Base {
    public:
        ~Base() { /* 基类析构逻辑 */ }
    };
    
    class Derived : public Base {
    public:
        ~Derived() { /* 子类析构逻辑 */ }
    };
    
    Base* obj = new Derived();
    delete obj; // 只调用 Base 的析构函数,Derived 的析构函数不会被调用
    
  2. 错误的对象管理

    • 如果使用了不当的对象管理策略(如使用原始指针而不是智能指针),可能会导致对象的重复释放或未释放。例如,如果一个对象被多个指针引用,可能会在一个指针释放后,其他指针仍然指向已释放的内存,导致未定义行为。
  3. 不正确的拷贝和赋值

    • 如果类没有正确实现拷贝构造函数和赋值运算符,可能会导致多个对象共享同一资源,进而在析构时出现问题(如双重释放)。
  4. 资源管理不当

    • 在多态情况下,子类可能会分配特定的资源(如动态内存、文件句柄等),如果这些资源没有在子类的析构函数中正确释放,也会导致内存泄漏。

为了避免这些问题,建议遵循以下最佳实践:

  • 在基类中使用虚拟析构函数。
  • 使用智能指针(如 std::unique_ptrstd::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++中类中所有函数都可以只声明不写函数体?

可以,编译的时候没问题,但是在调用的时候就会报错