1.0输入输出流

标准输入输出

c++

iostream cin,cout
文件输入输出

1
2
3
4
5
6
7
8
<fstream>
ofstream fout("out.txt")
fout<<str<<endl;
fout.close();
ifstream fin("in.txt")
string str1,str2;
fin>>str1>>str2;
fin.close();

格式化输入输出

1
<iomanip>

java

标准输入输出
System.out.println(“Hello World”);
Scanner scanner = new Scanner(System.in);
文件输入输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.Scanner;

PrintWriter fout = new PrintWriter(new FileWriter("out.txt"));
fout.println(str); // 将字符串写入文件并换行
fout.close(); // 关闭文件以确保写入完成

Scanner fin = new Scanner(new File("out.txt"));
String str1 = fin.next(); // 读取第一个单词
String str2 = fin.next(); // 读取第二个单词
fin.close();

格式化输入输出
String.format()
或 System.out.printf()

1.1 变量和动态内存分配

cpp

全局变量:可在不同cpp文件间共享,用extern应用别的文件变量
静态全局变量:static 修饰,只在本文件共享
静态局部变量:static 修饰,只本函数内共享,存储在全局区,第一次执行到定义时初始化,并在函数调用结束后仍然保留值,在后续调用中继续使用该值。
局部变量:栈中
动态分配变量 (allocated variable):使用动态内存分配( new )创建,存储在堆区,需要程序员手动释放
new执行类和对象的构造函数,malloc,delete删除指针指向的对象的值,而不是删除指针

1
2
3
4
5
6
7
8
9
10
11
12
T* p  = new T;          // 默认构造,内置类型不初始化;类类型走默认构造函数
p=&obj; //*是值,&是地址,new返回地址
T* p=&obj;
T* p2 = new T(); // 值初始化,内置类型置零;类对象走值初始化
T* p3 = new T{args…}; // 列表初始化(C++11)
T* arr = new T[N]; // 默认初始化 N 个元素
T* arr2 = new T[N](); // 值初始化
T* arr3 = new T[N]{val…}; // C++11:只允许聚合或有 constexpr 构造时的部分列举


delete p; // 与 new T; 配对
delete[] arr; // 与 new T[N]; 配对
1
2
3
4
5
6
#include <new>
T* p4 = new (place) T; // “放置 new”(placement new)
void* addr = std::malloc(sizeof(T));
T* obj = new (addr) T(args...); // 在现有内存上“原地”构造
obj->~T(); // 手动析构
std::free(addr); // 手动释放原始内存

不会分配内存,只调用构造函数。
不需要 / 不能 与 delete 搭配;析构后由调用方负责释放托管内存。

注意

内存泄露,多次delete,悬空指针
如果两个指针指向同一块堆内存,其中一个被 delete ,另一个再访问就成为悬空指针,若需要让多个指针共享所有权,应使用智能指针(见第6节)。

java

Java 中没有独立于类之外的全局变量概念。需要跨作用域共享的数据通常通过类的 静态变量 来实现(相当于类级别的全局变量)
成员变量:包括实例变量和静态变量。实例变量属于对象,随对象存储在堆上;静态变量用 static定义,属于本身,在整个应用运行期间存在(类似 C++ 的全局变量),对所有该类的对象共享,存储在方法区或堆的特殊区域。
局部变量:定义在方法或块内部,作用域仅限其中,生命周期随调用结束而结束。Java 的局部变量存储在栈上(对于原始类型)或作为引用存在栈上(引用类型的对象本身仍在堆上)。
动态分配:一切靠new.

1
2
Integer p = new Integer(123);
p = new Integer(456);

在cpp会导致原来那部分内存泄露,java不会,Integer(123) 一旦没有任何引用指向,就会被垃圾回收(具体回收时间不定)Java也不存在 C++ 中野指针、悬空指针的问题——如果引用超出作用域或被赋为 null ,之后便无法通过它访问对象。
在 Java 中,多个引用指向同一对象很常见,赋值只是复制引用,GC 会跟踪对象的引用计数或可达性,当没有引用时再释放对象,因此无须手动管理多引用场景。

1.2 引用

cpp

引用后就被绑定,必须连接到合法内存上的对象不能储存NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int *f(int *x) { // 接受指针,返回指针
(*x)++; // 修改指针所指变量
return x;
}
int &g(int &x) { // 接受引用,返回引用
x++; // 直接修改引用所指变量
return x;
}
int &h() {
return x; // 返回一个全局变量 x 的引用(假设 x 已定义)
}
int x;
int main() {
int a = 0;
f(&a); // 调用 f,需要取地址传递,或者传入地址
g(a); // 调用 g,直接传变量,函数内实际操作的是 a 本身
h() = 16; // h() 返回全局变量 x 的引用,可直接赋值修改 x
}

需要注意:返回局部变量的引用会产生悬空引用(对象已销毁),这一点和返回指针类似。

引用主要用于函数参数和返回值传递,提供类似指针的效率又避免了空指针错误,在需要“不改变原对象”或“直接修改原对象”时结合 const 使用非常方便。

java

没有指针,所有变量都看作引用,开发者无法操作内存。给引用重新赋值就不会影响原对象。引用 相当于传入一个指向对象地址值的东西。

1
2
3
4
5
6
7
void addOne(Integer x) {
x = x + 1; // 重新赋值,不影响外部引用
或x = new Counter();
}
void increment(IntWrapper wrapper) { // 假设 IntWrapper 是一个包装类
wrapper.value++; // 修改对象内容,影响外部对象
}

Java 则始终按值传递,基本类型按值拷贝,对象类型按引用拷贝。如果想避免函数修改传入对象,可以在函数中创建对象副本或在设计上使用不可变对象。
反之如果想在函数中“返回”一个修改后的原始数据,Java 要么通过返回值(返回新数据)要么传入可变对象来修改其字段。

1.3 const 类型

cpp

初始赋值后不可更改值,编译器常量

const和指针有多重组合形式

1
2
3
4
5
6
7
8
9
10

char * const q = "abc"; // q 是常量指针,q指向固定,但内容可通过 q 修改
*q = 'c'; // OK,修改指向内容(此处其实修改了字符串常量,运行时可能错误)
q++; // Error! q 本身是 const,不能改变指向

const char *p = "abc"; // p 指向常量数据
*p = 'c'; // Error! 不能通过 p 修改所指内容
p++; // OK,p 可指向下一个字符

const char * const r = "abc";

直接将字符串字面量赋给 char* 在 C++ 中是非标准行为,要么赋给指针常量,要么赋给数组,可以改变值char s2[] = "Hello"; // 或者用数组拷贝字面量,允许修改 s2 内容

const 成员函数

在成员函数签名后加 const ,表示该成员函数不会修改所属对象的任何数据成员。
只有当对象本身是非 const 时,才能调用非 const 的成员函数,无则调用const版本;而const 对象只能调用 const 成员函数 。如果类中同时定义了 const 与非 const 的同名成员函数,它们构成重载关系

1
2
3
4
5
6
7
8
9
10
11
class A {
public:
void foo() { cout << "A::foo()" << endl; }
void foo() const { cout << "A::foo() const" << endl; }
};
int main() {
A a;
a.foo(); // 调用非 const 版本
const A ca;
ca.foo(); // 调用 const 版本
}

const参数和返回值

函数形参可以声明为 const,表示在函数内部不修改该参数(如果是按引用或指针传递,则不修改原始对象)。函数返回值也可声明为 const,表示返回后不能修改返回的对象。例如返回 const 引用,可防止调用者通过引用修改内部数据。

java

变量

Java 没有 const 关键字用于变量或函数,但提供了功能近似的 final 关键字: 用 final 声明的变量初始化后不可再次赋值,相当于只读变量。
也可以把对象设计为不可变类,如String
与 C++ const 不同的是,Java 的 final 变量可以是类的成员,也可以是方法内的局部变量。对于引用类型, final 仅保证引用本身不可改变指向,但引用指向的对象内容若是可变的仍可修改(除非那些内容也有自己的限制)。就是相当于只有常量指针,没有指针常量。

final方法和类

将方法声明为 final ,则子类不能重写该方法。这在语义上与 C++ 中将方法声明为virtual 后又标记 override final (C++11 引入)相似,都是为了禁止进一步覆写。
将类声明为final 则表示该类不能被继承,类似于 C++11 中类声明后加 final 的效果。

没有不可变成员函数

1
2
3
4
public static final double PI =3.14159;
final StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 可以修改对象内容
// sb = new StringBuilder(); // ERROR: final 引用不能指向新对象

1.4 拷贝构造和赋值

cpp

如果将一个对象直接赋值给另一个新创建的对象,会调用拷贝构造函数;如果是先创建对象再赋值,则调用赋值运算符。拷贝构造函数一般定义形参为 const ClassName& ,这样才能接受 const 或非 const 对象。缺省情况下,编译器提供的拷贝构造和赋值运算符执行浅拷贝(逐成员拷贝) 。对于包含指针成员的类,这可能导致多个对象共享同一块内存(危险)。需要根据情况实现深拷贝(分配新内存复制内容) 。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Person {
char *name;
Person(const char *s) {
name = new char[strlen(s)+1];
strcpy(name, s);
}
~Person() { delete[] name; }
};
Person p1("Trump");
Person p2 = p1; // 调用拷贝构造,默认浅拷贝,只复制指针值
cout << (void*)p1.name << endl;
cout << (void*)p2.name << endl; // 输出相同地址,两个对象name指向同一块内存,name指针是重新分配的,指向的东西是同一个。
// 若要避免此问题,应自定义拷贝构造在此处分配新内存并复制字符串(深拷贝)

由于 p1 和 p2 拷贝后内部指针相同,销毁时会发生双重释放错误。因此,若类有资源需要深拷贝,必须重载拷贝构造和赋值运算符,以执行资源的复制和独立管理。C++11 起也可通过删除拷贝构造/赋值( =delete )

java

引用

在 Java 中,对象变量存储的是引用,赋值对象变量只会拷贝引用值,而不会复制对象本身(相当于浅拷贝地址)。因此不存在 C++ 的拷贝构造函数语义: Object obj2 = obj1; 在 Java 中只是让 obj2 引用与 obj1 相同的对象,并不创建新对象。

复制对象

1)想要“两个独立对象”,必须手动 new 并根据原对象内容赋值。
2)提供对象的克隆方法,实现Cloneable 接口并覆盖 clone()
3)提供拷贝构造函数或者静态工厂来自行实现深拷贝

操作符重载

由于没有拷贝构造和赋值重载,Java 也不存在赋值操作符重载的问题。

比较

用== 比较对象比较的是引用是否相同,类似C++比较指针地址;
要比较内容需要调用 .equals() 方法(类似C++中自定义的 operator== 重载,但Java中很多标准类已提供 .equals 实现)。

1.5 extern

Java 没有C++ extern 等概念,因为静态变量天然在整个应用类加载后可访问(权限允许的情况下)。

2 类

类是面向对象的基础,它将属性(成员变量)和操作(成员函数)封装在一起 。
任何实体都可被抽象为类的对象,程序就是对象之间通过方法调用和消息传递协作完成的
一个类可以被多次实例化生成多个对象,每个对象都有自己的一份实例成员数据。

cpp

C++ 中使用关键字 class 定义类,默认成员访问权限为 private ;也可使用 struct 定义,默认权限为public ,但在 C++ 中 struct 除了默认权限外与 class 无本质区别
类成员的定义在实现文件中需要使用作用域运算符 ClassName:: 来指明所属类 。

java

Java 的面向对象模型与 C++ 相似,也是用 class 定义类,将字段和方法封装.
主要区别在于源文件结构:Java 通常一个 .java 源文件只含有一个公共类(类名须与文件名匹配),类的定义和实现不分离,在同一个文件内编写。
Java 中用点号 . 访问类的成员(无论静态或实例),没有 C++ 中
的 :: 运算符
Java 有包 (package) 的概念,用类似目录的命名组织类名,点号也用于包名和类名之间。例如 java.util.List 表示 util 包下的 List 类。Java 使用 import 声明来引用其他包的类,而不是通过头文件包含。这样,Java 避免了C++头文件重复包含的问题,编译依赖管理更简单。 Java 的 import 只是告诉编译器“我要用哪个类”,而 C++ 的 #include 是把别人的源代码(可能包含实现、宏、模板)一股脑拷进来。
因而 Java 不会遇到头文件重复、编译单元暴涨、宏侧效应等问题,依赖图天然更小,增量编译、冲突排查和构建系统实现都更简单。

Java “符号引用”让编译器只关心 接口,不关心 实现文本
编译 Bar.java 时,若用到 pkg.Foo:编译器在类路径或模块路径找 Foo.class(已编)或 Foo.java(未编)。
只读取 Foo 的 符号表(字段、方法签名)即可继续;Foo 的方法体此刻不必看。因此 接口变了才会影响下游。方法体内部修改→下游类 无需重编译;链接在运行期由类加载器处理。
C++ 由于把函数实现也写进头文件 (inline/template) 的普遍做法,只要实现改动,包含它的所有 TU 都要重编译。

2.1 构造函数析构函数

cpp

构造函数

在创建对象时自动执行,用于初始化对象 。可以定义多个构造函数(构成重载)以应对不同初始化需求 。如果没有定义构造函数,编译器会提供一个默认构造函数。构造函数可以带参数,创建对象时需要传入。默认构造函数指无参数或参数都有默认值的构造函数

初始化列表
1
2
3
Sample(int x) : id(x) { // 用初始化列表将 id 初始化为 x
// 构造函数体
}

初始化列表的执行顺序按成员在类中声明的顺序,而非列表中出现的顺序。使用初始化列表还能提高性能(直接构造,而不是先默认构造再赋值)

拷贝构造函数
1
ClassName(const ClassName &other)
移动构造函数
1
ClassName(ClassName &&other) 

用于获取临时对象的所有权,避免不必要的拷贝

重载

定义无参构造,定义带参构造。
注意若定义了任何构造函数,编译器不再自动提供默认构造,若仍需无参构造必须显式定义。
还有const构造函数。

析构函数

类名前加 ~ ,在对象生命周期结束时自动调用(离开作用域、delete对象时) 。析构函数无参数、无返回值,每个类最多一个析构函数。
会默认生成

注意基类的析构

函数应声明为 virtual (虚函数)以保证通过基类指针删除派生类对象时调用正确的析构

java

用与类同名的函数作为构造方法,在new ClassName()的时候调用。可重载,不支持默认参数。
和cpp最大的区别是:Java 不允许直接调用构造方法创建临时对象(C++ 可以直接构造临时对象),必须使用 new 。
Java 没有语言层面的拷贝构造函数。如果需要复制对象,一般自行编写一个构造方法接收同类对象并复制其字段。
Java 没有显式析构函数。
C++ 把“释放资源”的逻辑写进析构函数,作用域一结束就自动执行;Java 只保证帮你回收内存,对文件、网络等必须用 try-with-resources 或手动 close(),否则就会泄漏。机制不同,但都是为了同一个目标——让资源生命周期在代码中被可靠地管理。

2.2 static类型成员

类成员分为实例成员和静态成员。
静态成员属于类本身,在所有对象间共享。

cpp

静态成员变量

在类内用 static 声明静态成员变量,但不分配存储。必须在类外定义并初始化一次

1
2
3
4
5
6
7
8
9
10
11
class A {
public:
static int count;
A() { A::count++; }
};
int A::count = 0; // 在类外初始化静态成员,无需再次写 static 但要加类名限定
int main() {
A *array = new A[100];
cout << A::count << endl; // 输出100,因为构造函数累加了100次
delete [] array;
}

静态成员变量在程序开始时分配,结束时释放,生命周期独立于任何对象。若未显式初始化,静态成员会被默认初始化为零(对内置类型)。

静态成员函数

用 static 声明的成员函数,不作用于特定对象上,没有隐含的 this 指针 。因此静态函数内只能访问静态成员,不能直接访问实例变量或实例函数
调用静态成员函数无需对象实例,可直接用 ClassName::FunctionName() 语法调用,或通过类名作用域限定调用。

在函数内用static修饰的局部变量

则该变量在函数调用间保持值(静态存储区),只在首次调用时初始化 。这与类静态成员不同的概念,但利用相同关键字。前文1.1节已讨论过静态局部变量的行为。

java

静态成员变量

和cpp一样,区别是定义和初始化在类内部就实现,不需要在类外另行定义。
访问静态变量通常直接通过类名,例如 ClassName.staticVar 。也可以通过对象引用访问(如obj.staticVar ),但会被编译器转为类名访问形式。

1
2
3
4
5
6
7
8
9
10
11
12
class A {
static int count = 0;
A() { count++; }
}
public class Main {
public static void main(String[] args) {
System.out.println(A.count); // 0,尚未创建对象
A a1 = new A();
A a2 = new A();
System.out.println(A.count); // 2,两个对象共享同一个 count
}
}

静态方法

用 static 修饰的方法,不依赖对象实例。调用时直接用类名即可,如Math.max(a,b) 。也没有this无法访问非静态成员。只能使用传入参数或静态变量,不能访问实例成员除非被当做参数传入。静态方法常用来实现工具类方法(如 java.lang.Math 的静态方法。

静态块

Java 类可以包含静态初始化块,在类加载时执行一次,用于复杂静态变量初始化。这在C++中通常通过全局对象或函数静态对象实现。

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
27
28
29
30
31
32
33
34
35
36
import java.util.HashMap;
import java.util.Map;

public class ErrorCodes {

// 1. 声明一个静态的 Map 变量
public static final Map<Integer, String> codeToMessage;

// 2. 使用静态初始化块来初始化这个 Map
static {
System.out.println("Java 静态块执行中... (仅执行一次)");
// 创建一个临时的、可变的 Map
Map<Integer, String> tempMap = new HashMap<>();

// 模拟复杂的初始化逻辑,例如从文件或数据库加载
tempMap.put(404, "资源未找到 (Resource Not Found)");
tempMap.put(500, "服务器内部错误 (Internal Server Error)");
tempMap.put(401, "未经授权 (Unauthorized)");

// 将其变为不可修改的 Map 并赋值给静态变量
codeToMessage = Map.copyOf(tempMap);
}

// 一个静态方法来使用这个 Map
public static String getMessage(int code) {
return codeToMessage.getOrDefault(code, "未知错误码 (Unknown Error Code)");
}

public static void main(String[] args) {
System.out.println("main 方法开始执行...");
System.out.println("错误码 404: " + ErrorCodes.getMessage(404));
System.out.println("错误码 500: " + ErrorCodes.getMessage(500));
System.out.println("再次调用不会重复初始化...");
System.out.println("错误码 401: " + ErrorCodes.getMessage(401));
}
}

C++ 没有直接对应的 static { … } 语法。

我们通过一个私有的静态函数 getCodeMap() 来封装初始化。

函数内的 static const CodeMap instance 是核心。它只会在 getCodeMap() 第一次被调用时才进行初始化。这是一种延迟初始化(Lazy Initialization)。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <map>
#include <string>

class ErrorCodes {
public:
// 一个静态方法来获取错误信息
static const std::string& getMessage(int code) {
// 通过调用辅助函数来获取单例的 Map
const auto& messageMap = getCodeMap();
auto it = messageMap.find(code);
if (it != messageMap.end()) {
return it->second;
}
static const std::string unknown = "未知错误码 (Unknown Error Code)";
return unknown;
}

private:
// 定义 Map 的类型别名
using CodeMap = std::map<int, std::string>;

// 辅助函数,其内部的静态对象只会被初始化一次
static const CodeMap& getCodeMap() {
// 3. 函数内的静态对象。它在第一次调用 getCodeMap() 时被初始化。
// 这是线程安全的 (自 C++11 起)。
static const CodeMap instance = [] {
std::cout << "C++ 函数内静态对象初始化中... (仅执行一次)" << std::endl;
CodeMap tempMap;
// 模拟复杂的初始化逻辑
tempMap[404] = "资源未找到 (Resource Not Found)";
tempMap[500] = "服务器内部错误 (Internal Server Error)";
tempMap[401] = "未经授权 (Unauthorized)";
return tempMap;
}(); // 使用 IILE (立即调用的 Lambda 表达式) 来执行初始化逻辑

return instance;
}
};

int main() {
std::cout << "main 函数开始执行..." << std::endl;
std::cout << "错误码 404: " << ErrorCodes::getMessage(404) << std::endl;
std::cout << "错误码 500: " << ErrorCodes::getMessage(500) << std::endl;
std::cout << "再次调用不会重复初始化..." << std::endl;
std::cout << "错误码 401: " << ErrorCodes::getMessage(401) << std::endl;
return 0;
}

缺少

但C++还多了静态局部和自由静态函数的概念(后者是命名空间级的函数,与类无关),Java 则没有全局函数,一切函数不是实例方法就是静态方法属于某类。

2.3 内联函数

cpp

关键字 inline 可以建议编译器在调用处展开函数体,以减少函数调用开销 。内联是在编译阶段完成的优化,相当于宏替换但更安全(有类型检查)。内联函数的定义通常需要在每个使用的地方可见(因此常定义在头文件)。
类内部定义的成员函数默认就被视为内联函数(编译器可自行决定是否内联)
本质是空间换时间
现代编译器会自动进行内联优化,无论函数是否标记 inline,只要符合优化策略。相反,即使标记 inline,如果函数体过于复杂,编译器也可能选择不内联。另外,递归函数通常不会内联(或只内联部分层次),因为展开会无限增大代码。过度使用内联会导致可执行文件增大。C++17 起内联关键字在 ODR(One Definition Rule)上也有特殊作用(如内联变量),但本节聚焦函数。

java

Java 没有显式的 inline 关键字。早期 JVM 对于频繁调用的小方法,会在 JIT(Just-In-Time)编译阶段自动将其内联优化。这完全由 JVM 来决定。不过,Java 有些替代手段,例如将方法定义为 private 或 static 且简单,JIT优化时更倾向于内联这些可预测的方法。此外,JVM 会基于性能监测(如某方法调用次数超过一定阈值)进行内联优化,这比静态编译器更动态智能。
需要说明的是,Java 字节码层面每次方法调用都是一个指令,例如 invokevirtual 或 invokestatic ,而是否内联由JIT在运行时动态优化决定,开发者不需也不能干预。所以Java开发中不会显式讨论某方法是否内联,但JIT优化技术确保性能接近C++的优化水平。

对比

C++ 程序员通过 inline 控制编译期优化,而 Java 依赖JVM在运行期的优化。C++ 内联可减少函数调用开
销但会增加编译时间和可能代码膨胀;Java 的动态内联由JVM智慧管理,既保证性能又减少人为失误。现代C+
+与Java在优化上逐渐趋同——都更依赖优化器而非手工提示。不过,在高性能场景下,C++ 开发者仍有比
Java 更直接的控制权。

2.4 继承

继承 vs 组合: 继承( class Derived : public Base )意味着 Derived 是一种特殊的 Base,继承Base 的属性和行为 。组合则是在类中定义成员对象,将其他类作为自己的组成部分。两者都是复用代码的手段:继承易于表现“是一个”关系,组合表现“有一个”关系。

cpp

C++ 支持单继承和多重继承(一个派生类可有多个直接基类)

继承的成员可见性

派生类继承了基类的所有成员(包括私有成员,只是私有成员不能直接访问)。基类 public 成员在派生类中保持 public , protected 成员在派生类中仍是 protected ,private 成员则始终无法直接访问(只能通过基类的公有/受保护方法访问)
C++ 有三种: public 继承(常用,保持基类接口), protected 继承(基类 public 成员变成protected), private 继承(基类 public/protected 成员均变成 private)。默认继承方式: class默认私有继承, struct 默认公有继承。

菱形继承

当一个类同时继承的两个父类,而这两个父类又继承自同一个“祖父”类时,继承关系图看起来就像一个菱形
为了解决这个问题,引入了虚继承(Virtual Inheritance)。通过在继承时使用 virtual 关键字,可以告诉编译器,对于共同的基类(Machine),只保留一个实例

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>

class Machine {
public:
long power_level;
Machine() : power_level(1) {
std::cout << "Machine constructor called." << std::endl;
}
};

// Printer 使用 virtual 继承 Machine
class Printer : public virtual Machine {
public:
Printer() {
std::cout << "Printer constructor called." << std::endl;
}
};

// Scanner 也使用 virtual 继承 Machine
class Scanner : public virtual Machine {
public:
Scanner() {
std::cout << "Scanner constructor called." << std::endl;
}
};

// Copier 继承 Printer 和 Scanner
// 最派生的类(Copier)现在负责构造虚基类(Machine)
class Copier : public Printer, public Scanner {
public:
Copier() { // Machine() 会在这里被 Copier 的构造函数直接调用
std::cout << "Copier constructor called." << std::endl;
}
};

int main() {
Copier myCopier;

// 现在可以直接访问,不再有歧义!
myCopier.power_level = 100;

std::cout << "Size of Copier object: " << sizeof(myCopier) << " bytes" << std::endl;
std::cout << "Power level: " << myCopier.power_level << std::endl;

// 两个路径指向同一个变量
myCopier.Printer::power_level = 150;
std::cout << "Power level via Scanner path: " << myCopier.Scanner::power_level << std::endl;

return 0;
}

Machine 的构造函数现在只被调用了一次!这是由最派生的类 Copier 的构造函数负责调用的,确保了 Machine 子对象只被初始化一次。
假如没有虚继承,那么Machine 的构造函数被调用了两次。这证明 myCopier 对象内部包含了两个独立的 Machine 子对象。

构造和析构的顺序

创建派生类对象时,会先调用基类构造函数(若有多级继承,按继承链从上到下调用),然后按声明顺序构造派生类的成员对象,最后调用派生类自己的构造函数体
析构顺序与构造相反:先执行派生类析构函数体,然后依次调用成员对象析构,最后调用基类析构 。这保证基类部分始终在派生部分之前正确建立和销毁。

如果派生类没有定义构造函数,将隐式调用基类的默认构造函数 。
如果派生类定义了构造函数但未显式调用基类构造,编译器会尝试调用基类的默认构造函数 。因此,基类最好提供无参构造或派生类需调用特定构造,以避免编译错误。

当继承和组合两种情况同时出现时,先构造基类,再构造派生类中组合的其他类,再构造派生类。就算组合的类在初始化列表或者构造函数中没有调用构造函数,C++ 编译器也会自动调用这个类默认的构造函数

派生类显式调用基类构造
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
public:
int i;
A(int ii = 0) : i(ii) { cout << "A(): " << i << endl; }
~A() { cout << "~A()" << endl; }
};

class B : public A {
public:
int j;
A a; // B 中包含一个 A 类成员对象
B(int ii = 0) : A(), j(ii) {
cout << "B(): " << j << endl;
}
~B() { cout << "~B()" << endl; }
};
int main() {
B b(100);
}

假设以上构造函数输出调试信息,则执行 B b(100) 时的输出顺序可能是:

1
2
3
4
5
6
A(): 0 // 基类 A 的构造(B未显式调用基类带参构造,默认调用无参)
A(): 0 // 成员对象 a(类型A)的构造
B(): 100 // 派生类 B 自身的构造
~B() // 作用域结束,调用 B 析构
~A() // 成员对象 a 析构
~A() // 基类 A 析构
隐式调用成功 (基类有无参构造函数)
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
27
28
29
30
31
32
#include <iostream>
#include <string>

// 基类
class Vehicle {
public:
std::string name;

// 1. 基类有一个无参/默认构造函数
Vehicle() : name("未知车辆") {
std::cout << "基类 Vehicle 的【无参】构造函数被调用。" << std::endl;
}
};

// 派生类
class Car : public Vehicle {
public:
int numberOfDoors;

// 2. 派生类的构造函数,没有显式调用基类构造函数
Car(int doors) : numberOfDoors(doors) {
// 在这里的初始化列表之后,编译器会自动插入对 Vehicle::Vehicle() 的调用
std::cout << "派生类 Car 的构造函数被调用。" << std::endl;
this->name = "小汽车"; // 可以修改继承来的成员
}
};

int main() {
Car myCar(4);
std::cout << "创建的车辆是: " << myCar.name << ", 有 " << myCar.numberOfDoors << " 个门。" << std::endl;
return 0;
}

输出:

1
2
3
基类 Vehicle 的【无参】构造函数被调用。
派生类 Car 的构造函数被调用。
创建的车辆是: 小汽车, 有 4 个门。
编译失败 (基类无默认构造函数)
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
27
28
29
30
#include <iostream>
#include <string>

class Vehicle {
public:
std::string name;

// 1. 基类【只】有一个需要参数的构造函数
// 因此,编译器不再提供默认的无参构造函数 Vehicle()
Vehicle(std::string n) : name(n) {
std::cout << "基类 Vehicle 的【有参】构造函数被调用。" << std::endl;
}
};

class Car : public Vehicle {
public:
int numberOfDoors;

// 2. 派生类的构造函数,没有显式调用基类构造函数
Car(int doors) : numberOfDoors(doors) {
// 编译错误发生在此!
// 编译器尝试隐式调用 Vehicle::Vehicle(),但它不存在。
std::cout << "派生类 Car 的构造函数被调用。" << std::endl;
}
};

int main() {
// Car myCar(4); // 这行代码将导致编译失败
return 0;
}
1
2
error: no matching function for call to 'Vehicle::Vehicle()'
note: candidate constructor not viable: requires 1 argument, but 0 were provided

编译器不知道如何构造 Vehicle 部分,因为它找不到一个不需要参数的 Vehicle 构造函数。它不会猜测应该给 Vehicle(std::string n) 传递什么字符串。

派生类构造函数必须承担起责任,显式地告诉编译器应该调用基类的哪一个构造函数,并提供必要的参数。

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
#include <iostream>
#include <string>

class Vehicle {
public:
std::string name;
Vehicle(std::string n) : name(n) {
std::cout << "基类 Vehicle 的【有参】构造函数被调用。" << std::endl;
}
};

class Car : public Vehicle {
public:
int numberOfDoors;

// 3. 在构造函数的初始化列表中,显式调用基类的有参构造函数
Car(std::string name, int doors) : Vehicle(name), numberOfDoors(doors) {
std::cout << "派生类 Car 的构造函数被调用。" << std::endl;
}
};

int main() {
Car myCar("特斯拉", 4); // 现在可以成功创建对象了
std::cout << "创建的车辆是: " << myCar.name << ", 有 " << myCar.numberOfDoors << " 个门。" << std::endl;
return 0;
}

方法覆盖 (Overriding)

派生类可以重定义继承自基类的成员函数。
C++中,如果基类方法非虚函数,这不是严格的“覆盖”,而是隐藏.如果派生类定义了与基类同名但不同参数的函数,基类的同名函数会被隐藏——要使用基类版本需加类名限定或 using Base::functionName 引入
若想实现多态,基类函数必须声明为 virtual ,详见后面。

访问控制

C++ 使用 public 、 protected 、 private 来控制成员访问权限 。
public成员任何地方可访问;protected成员对子类和友元可见;private成员仅本类和友元可见

friend

在类中声明一个全局函数或者其他类的成员函数为 friend,可以使这些函数拥有访问类内 private 和 protected 类型的变量和函数的权限.友元函数也可以是一个类,这种情况下被称为是友元类,整个类和所有的成员都是友元.

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
private:
int val;
public:
A(int value): val(value) {
cout<<"A()"<<endl;
}
friend void showValue(A a);
};
void showValue(A a)
{
cout<<a.val<<endl;
}

继承方式也会影响这些权限在派生类中的表现,如前述。

java

extends

class B extends A { … }
所有非 final类都可被继承。

final

ava 用 final 关键字防止继承或覆盖。标记为 final 的类不能有子类(如java.lang.String ),final 方法不能被子类覆盖。这和C++中 final (C++11) 的用法类似。不过C++final 出现较晚,Java 从一开始就有。

interfaces

为实现多继承的某些好处,Java 提供接口。接口可以理解为只有纯抽象方法(Java8之前)的集合,相当于C++中的抽象类且所有方法纯虚。
当一个类 implements 多个接口时,就相当于获得了多继承的效果

比如,抽象类 - 定义“是什么”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Animal.java
public abstract class Animal {
// 1. 实例字段:抽象类可以有自己的状态(属性)。接口不能。
protected String name;

// 2. 构造方法:用于初始化通用属性。
public Animal(String name) {
this.name = name;
}

// 3. 具体方法:所有子类共享的、已实现的功能。
public void eat() {
System.out.println(name + " 正在吃东西。");
}

// 4. 抽象方法:强制所有子类必须提供自己的实现。
// 它定义了一个规范,但没有提供具体实现。
public abstract void makeSound();
}

Animal 类不能被实例化 (new Animal(“…”) 会编译错误)。

接口 - 定义“能做什么”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Flyable.java
public interface Flyable {
// 1. 接口中所有变量默认都是 public static final 常量
int MAX_FLY_SPEED = 100;

// 2. 抽象方法:任何实现此接口的类都必须提供 fly() 的实现
void fly();

// 3. Default 方法 (Java 8+): 提供一个默认实现,实现类可以不覆盖它
default void takeOff() {
System.out.println("准备起飞!");
}
}

// Swimmable.java
public interface Swimmable {
void swim();
}

接口没有实例字段和构造方法,因为它不代表一个具体的“物体”,只代表一种“能力”。

类:组合

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
27
28
29
30
31
32
// Dog.java 一个简单的子类
public class Dog extends Animal { // 继承 Animal
public Dog(String name) {
super(name);
}

@Override
public void makeSound() { // 必须实现父类的抽象方法
System.out.println(name + " 在汪汪叫!");
}
}
// Duck.java 继承并实现多个接口 (多继承效果)
public class Duck extends Animal implements Flyable, Swimmable { // 继承并实现多个接口
public Duck(String name) {
super(name);
}

@Override
public void makeSound() {
System.out.println(name + " 在嘎嘎叫!");
}

@Override
public void fly() {// 必须实现接口的抽象方法
System.out.println(name + " 正在低空飞行。");
}

@Override
public void swim() {// 必须实现接口的抽象方法
System.out.println(name + " 正在水上游泳。");
}
}

成员继承

Java 只支持单一继承:一个类只能直接继承一个父类(除 Object 无父类)
不支持多重继承是为了避免菱形继承的模糊和复杂性
Java 引入 接口 (interface)机制,实现类似多继承的效果(详见下文)
Java 没有像 C++ 那样的继承权限级别(public/protected/private 继承)——类继承关系对外总是公开的,访问权限由成员自身的修饰符决定。也就是说,Java 不允许你继承一个类却降低其接口中方法的可见性。
当一个类既不在同一个包,又不是子类时,它无法访问 protected 成员,但可以访问 public 成员。当你希望一个方法或变量不作为公共接口,但又希望为子类提供一个可扩展或可定制的“钩子”时,使用 protected。
同时,父类的private对于子类是不可见的。子类如果与父类不在同一个包里,则父类的包级成员(无修饰符)对子类也不可见。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 父类 Animal
class Animal {
protected void eat() {
System.out.println("动物正在吃东西...");
}
}

// 子类 Cat
class Cat extends Animal {

// 正确:使用 public,可见性高于 protected
@Override
public void eat() {
System.out.println("小猫正在优雅地吃猫粮...");
}

/*
// 正确:使用 protected,可见性等于 protected
@Override
protected void eat() {
System.out.println("小猫正在吃东西...");
}
*/

/*
// 编译错误! attempting to assign weaker access privileges; was protected
// 企图赋予防问权限更弱的修饰符;父类是 protected
@Override
void eat() { // 'void' 前面不写,是默认的包级私有(package-private)权限,低于 protected
System.out.println("错误的覆盖方式");
}
*/

/*
// 编译错误! attempting to assign weaker access privileges; was protected
@Override
private void eat() { // private 权限最低
System.out.println("错误的覆盖方式");
}
*/
}

public class Main {
public static void main(String[] args) {
Animal myAnimal = new Cat(); // 用父类引用指向子类对象
myAnimal.eat(); // 如果 Cat 的 eat() 方法是 private 或包级私有,这里就无法调用了
}
}

构造与初始化

Java 子类构造方法的首语句必须调用父类构造(默认调用无参父构造),否则编译错误。父类构造执行完后,才执行子类自己的初始化。这一点上Java与C++一致,只是语法不同.
而且先字段构造,再调用构造方法。所有字段都确定初始值。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// === 父类 ===
class GameCharacter {
// 1. 字段初始化:在构造方法执行前,name 会被赋予 "默认角色"
public String name = "默认角色";
private int health = 100; // 私有字段

// 2. 父类构造方法
public GameCharacter() {
// 子类构造方法中的 super() 会调用这里
System.out.println("2. 父类 GameCharacter 构造方法执行");
System.out.println(" 此时父类 name = " + this.name + ", health = " + this.health);
}

// 3. 将被覆盖的方法 (默认就是虚方法)
public void attack() {
System.out.println(this.name + " 发起了攻击!");
}

// 4. 将被重载的方法
public void useAbility(String abilityName) {
System.out.println("角色使用了技能: " + abilityName);
}

// 5. 用于演示协变返回类型
public GameCharacter findAlly() {
System.out.println("找到了一个盟友!");
return new GameCharacter();
}
}

// === 子类 ===
class Warrior extends GameCharacter {
// 1a. 字段隐藏 (Shadowing): 子类声明了与父类同名的字段。不推荐!
public String name = "格罗姆·地狱咆哮";

// 1b. 子类自己的字段初始化
private String weapon = "血吼";

// 2a. 子类构造方法
public Warrior() {
// 首语句必须是 super() 或 this()。这里编译器会隐式添加 super();
super();
System.out.println("4. 子类 Warrior 构造方法执行");
// 注意:这里访问的 this.name 是子类自己的 name
System.out.println(" 此时子类 name = " + this.name + ", weapon = " + this.weapon);
}

// 3a. 方法覆盖 (@Override 注解帮助检查)
@Override
public void attack() {
// super 关键字调用父类被覆盖的方法
super.attack();
System.out.println(this.name + " 用 " + this.weapon + " 发动了斩杀!");
}

// 4a. 方法重载 (Overloading): 与父类方法同名,但参数不同
public void useAbility(int rageCost) {
System.out.println("战士消耗 " + rageCost + " 点怒气使用了技能!");
}

// 5a. 协变返回类型:返回类型 Warrior 是父类方法返回类型 GameCharacter 的子类型
@Override
public Warrior findAlly() {
System.out.println("战士找到了一个盟友,他也是个战士!");
return new Warrior();
}
}

运行

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
System.out.println("1. 开始创建 Warrior 对象...");
new Warrior();
System.out.println("5. Warrior 对象创建完毕!");
}
}

输出

  1. 开始创建 Warrior 对象…
  2. 父类 GameCharacter 构造方法执行
    此时父类 name = 默认角色, health = 100
  3. 子类 Warrior 构造方法执行
    此时子类 name = 格罗姆·地狱咆哮, weapon = 血吼
  4. Warrior 对象创建完毕!

方法覆盖

Java 中所有非静态、非私有方法默认即为虚方法 (即支持动态绑定),不需要像 C++ 那样显式声明 virtual。
Java 提供@Override 注解,可让编译器检查这是一个有效的覆盖.@Override 注解保证了如果我们把子类方法名写错(如 attak()),编译器会报错,防止了我们意外地创建了一个新方法而非覆盖。
如果子类定义了父类中不存在的方法重载,同C++一样,这只是新增方法,不影响父类的。需要注意的是,Java 若定义了与父类同名不同参数的方法,子类会同时拥有父类和自己的版本。cpp会遮蔽父类的所有同名方法(包括不同参数的)
可以通过在子类中显式调用父类方法super.methodName(args) 来使用父类版本。
运行

1
2
3
4
public static void main(String[] args) {
GameCharacter hero = new Warrior(); // 父类引用指向子类对象
hero.attack(); // 动态绑定:运行时调用 Warrior 的 attack() 方法
}

hero 的编译时类型是 GameCharacter,但运行时类型是 Warrior

字段隐藏

如果子类声明了与父类同名的字段,这在Java中是合法的字段隐藏…父类引用访问的是父类字段,子类引用访问的是子类字段。
运行

1
2
3
4
5
6
7
8
public static void main(String[] args) {
GameCharacter hero = new Warrior();
Warrior warrior = new Warrior();

// 字段访问取决于引用类型,而不是对象实际类型
System.out.println("通过父类引用访问 name: " + hero.name); // 输出 "默认角色"
System.out.println("通过子类引用访问 name: " + warrior.name); // 输出 "格罗姆·地狱咆哮"
}

这与方法覆盖完全不同!字段访问是在编译时根据引用的类型决定的。字段隐藏是极其不推荐的做法,它破坏了直觉,极易引发混淆。

协变返回

返回类型必须是相同或其子类型

1
2
3
4
5
public static void main(String[] args) {
Warrior warrior = new Warrior();
Warrior ally = warrior.findAlly(); // 返回值可以直接赋值给子类引用,无需强制类型转换
System.out.println("找到的盟友是: " + ally.name);
}

Warrior 类的 findAlly() 方法返回 Warrior 类型,它是 GameCharacter 类的 findAlly() 方法返回类型 GameCharacter 的子类。这是合法的覆盖。这带来了便利,调用者可以直接得到更具体的子类型,而不需要像 Warrior ally = (Warrior)character.findAlly(); 这样进行强制类型转换。

2.5 friend

cpp

友元并非类的成员,而是一种单向声明关系 :A 将某函数/类声明为友元后,该函数/类可不受限制地访问A的私有和保护成员。但友元函数本身不是 A 的成员,不需要 A:: 限定来定义,也不能通过 A 的对象调用(除非本身正是一个成员函数调用)。友元常用于运算符重载(如重载 << 操作输出私有数据)或两个类紧密协作的情况。友元破坏了封装(因为暴露内部实现给特定实体),但在必要时提供了便利。需要慎用以免增加耦合。
如前

java

Java 没有 friend 关键字。Java 的访问控制无法破例允许非成员访问私有成员。
如果需要类似功能,通常通过包级私有机制或公有存取方法来实现
包粒度较大,所有同包类都能访问,而 friend 在C++中可以精确到一个类或函数。
更常见的是,通过提供 public 的 getter/setter 或其它方法来间接访问私有成员。需要跨类访问时,可以设计合理的接口。例如,要让类 B 读取类 A 的私有字段,可以在 A 中提供 public getVal() 方法返回该字段,B 调用即可。
反射 (Reflection): 虽然正常情况下无法访问私有成员,但 Java 提供反射API ( java.lang.reflect )可以在运行时绕过权限检查强行访问/修改私有字段和方法(通过 setAccessible(true)。这是一种非常规手段,类似于C++用指针强制改内存,但比友元更不安全且不推荐。它主要用于框架或工具需要访问对象内部状态的场景。

友元违反了面向对象封装原则

2.6 多态

cpp

virtual

静态绑定 vs 动态绑定: 默认情况下,C++ 函数调用是静态绑定的,也就是在编译时确定函数地址

1
2
3
4
5
6
7
class A { public: void foo() { cout<<1; } };
class B: public A { public: void foo() { cout<<2; } };
A a; B b;
a.foo(); // 输出1,调用 A::foo()
b.foo(); // 输出2,调用 B::foo(),但这仍是静态决定的,因为对象类型已知
A *p = &b;
p->foo(); // 输出1,因为 A::foo() 不是虚函数,编译时类型p是 A*,绑定 A::foo()

即使 p 指向的是 B 对象,调用 p->foo() 也执行 A 的版
本 。这就是静态绑定,根据指针的静态类型(A*)决定调用 A 的函数。
在函数声明前加上关键字 virtual ,即可将其设置为虚函数,实现动态绑定 。动态绑定意味着真正调用的函数版本取决于对象的实际类型,而非指针/引用的静态类型

1
2
3
4
5
class A { public: virtual void foo() { cout<<1; } };
class B: public A { public: void foo() override { cout<<2; } };
A *p = new B();
p->foo(); // 输出2,调用 B::foo(),因为 foo 是虚函数
delete p;

这样就实现了 运行时多态:基类指针 p 在运行时判断出指向的是 B 对象,于是调用 B 的实现
通过维护一张虚函数表 (vtable) 来实现这一点,每个含虚函数的类有一张表存放其虚函数地址,每个对象存放一个指向所属类虚表的指针(通常称为 vptr ) 。调用虚函数时,通过对象的 vptr 找到对应函数地址再调用 。因此虚函数调用比普通函数略有开销(一次指针查找),但换来灵活性。

重要规则: 若一个类有任何可能被继承并通过基类指针删除的可能,其析构函数应定义为虚函数 。否则,通过基类指针删除派生对象时,只会调用基类析构,不会调用派生类析构,导致资源泄漏或未完全释放

纯虚函数

virtual ReturnType func(...) = 0;
要求派生类必须override提供具体实现,用于避免因函数签名不一致而没有实际覆盖的错误.

指针/引用

无论虚函数与否,直接用对象调用时调用该对象所属类的函数。
虚函数多态只有在使用基类指针或引用指向派生对象时才有意义
非虚函数:通过指针/引用调用时,绑定取决于指针/引用的编译时类型。基类指针调用基类实现,派生类指针调用派生实现(如果该函数在派生类中也定义,但这其实是隐藏不是重写)。
虚函数:通过指针/引用调用时,绑定取决于对象的运行时类型。指针指向基类对象则调用基类版本,指向派生对象则调用派生版本 。如果派生类未覆盖该虚函数,则调用基类的实现。派生类指针不能指向基类对象(除非强制转换,结果未定义行为)

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
class B {
public:
void f() { cout << "B::f" << endl; }
virtual void vf() { cout << "B::vf" << endl; }
void ff() { vf(); f(); } // 非虚 ff 调用 vf(虚) 和 f(非虚)
virtual void vff() { vf(); f(); } // 虚 vff 调用 vf 和 f
};
class D : public B {
public:
void f() { cout << "D::f" << endl; }
void vf() { cout << "D::vf" << endl; }
void ff() { f(); vf(); } // 非虚 ff 重定义
// 没有定义 vff,继承B::vff
};
int main() {
D d;
B* pb = &d;
pb->f(); // 非虚,静态绑定B::f => 输出 "B::f"
pb->ff(); // 非虚,静态绑定B::ff,但B::ff内部调用vf(虚)和f(非虚)
// vf()动态调 => D::vf 输出"D::vf"; f()静态调 => B::f 输出"B::f"
pb->vf(); // 虚,动态绑定D::vf => 输出 "D::vf"
pb->vff(); // 虚,动态绑定D没有override则调用B::vff
// B::vff内部vf()动态 => D::vf 输出"D::vf"; f()静态 => B::f 输出"B::f"
}

上面输出顺序应为:
B::f
D::vf
B::f
D::vf
D::vf
B::f

这可见:

  • pb->f() 因非虚所以用了 B::f,即使实际对象是 D。
  • pb->ff() 绑定到 B::ff 实现,其中 vf() 调用了 D::vf(动态绑定), f() 调用了 B::f(ff是B的成员函数,
    调用f名称绑定在编译期,此时ff是B类成员函数,所以f解析为B::f并静态绑定) 。
  • pb->vf() 调用 D::vf(虚函数动态绑定)。
  • pb->vff() 绑定到 B::vff(因为D没有override vff),vff内部按照其自身代码:调用 vf() 动态绑定D::vf,
    调用 f() 静态绑定B::f 。
  • 这些规则在C++中必须理解,否则会产生误用。Java 则简单得多,因为默认所有实例方法都是虚的,且不存在
    调用非虚版本的陷阱:Java中类似 B.ff() 内部的 vf() 和 f() 由于都是虚方法(除非 f 是 private/final),所
    以都会动态绑定到 D 实现。因此 Java 上述例子对应代码输出将是:
    D::f
    D::vf
    D::vf
    D::vf
    D::vf
    D::vf

区别

Java 将多态作为基本行为,更符合“里氏替换原则”,开发者无需额外关键词控制,大大降低了因忘记声明 virtual 导致错误的风险 。C++ 则提供更精细的控制:可以决定某些函数不允许覆盖(缺省情况),以获得优化;只有需要动态绑定时才付出代价。

2.7 强制类型转化

cpp

static_cast<新类型>(expr) : 用于各种显式类型转换,如数值基本类型间转换,类层次间向上转换(安全)或向下转换(需开发者确保正确) 。static_cast 不执行运行时类型检查,向下转型不安全,错误转换会导致未定义行为。它不能移除 const 限定,也不能转换不相关的类型(除非有转换运算符或构造函数定义)。但 static_cast 可用于将任何指针转换为 void* ,或从 void* 转回原类型指针。

const_cast<新类型>(expr) : 专门用于增加或去除 const/volatile 限定。可将 const T* 转为T* ,或 const T& 转为 T& 。使用场景如:某API函数参数没声明 const,但我们只有 const 数据,可用 const_cast 去掉 const 调用。不过如果强转掉 const 后修改数据原本是常量,会导致未定义行为。因此除非确信对象本身不是常量,才能安全地去掉 const。

reinterpret_cast<新类型>(expr) : 用于低级转换直接按位重新解释一个指针或整数。典型用法如将指针转换成整数类型(比如打印地址)或反之。它可以在不相关的指针类型之间转换,如把一个对象指针转成完全不相干类型的指针。但结果取决于实现并不一定有意义,常用于底层驱动、序列化需要。reinterpret_cast 是最不安全的转换,一般高层代码少用。

dynamic_cast<新类型>(expr) : 用于安全的向下转换和跨类层次转换。它只能用于含有虚函数的多态类(必须有 RTTI 信息),其原理是在运行时检查对象真实类型 。如果 expr 所指对象实际是新类型或其子类型,则 dynamic_cast 返回该类型的指针,否则返回 nullptr (对引用类型转换失败则抛出 std::bad_cast 异常)。dynamic_cast 不能用于基本数据类型或不相关的类间转换 。和static_cast 相比,dynamic_cast 向下转换更安全,但需运行时开销。若基类没有虚函数(无RTTI),dynamic_cast 编译就会报错

1
2
3
4
5
6
7
8
9
class A { public: virtual ~A(){} };
class B: public A {};
A a; B b;
A *ap = &a;
if(dynamic_cast<B*>(ap)) { cout << "OK1\n"; } else { cout << "Fail\n"; }
if(static_cast<B*>(ap)) { cout << "OK2\n"; } else { cout << "Fail\n"; }
ap = &b;
if(dynamic_cast<B*>(ap)) { cout << "OK3\n"; }
if(static_cast<B*>(ap)) { cout << "OK4\n"; }

假设 A 为多态基类(有虚析构),运行时:

  • OK1 判断输出 “Fail”,因为 ap 实际指向 A 对象,无法 dynamic_cast 成 B(返回null)。
  • OK2 判断输出 “OK2”,因为 static_cast(ap) 编译器允许,返回非空指针,但其实指向的内存不是一个真正的 B 对象,这个指针是无效的但不为null,于是 if判断为真。这证明 static_cast 下行转换不做检查,危险。
  • OK3 判断输出 “OK3”,ap 指向 B 对象时 dynamic_cast 成功,返回有效 B*。
  • OK4` 判断输出 “OK4”,ap 指向 B 对象时 static_cast 也返回 B*(其实就是原指针地址),自然非空。

上述结果 表明:dynamic_cast 在类型不匹配时返回 NULL 指示失败,而 static_cast 不管三七二十一转换指针类型,可能产生悬空的无效指针却依然非空,从而造成程序误判 。因此,建议在需要安全下行转换时使用 dynamic_cast,并确保基类带有虚函数使其可用。static_cast 只在明确知道转换合法时使用,如向上转换或基本类型转换。reinterpret_cast 则尽可能避免,除非做底层操作。

java

Java 的类型转换分两类:基本类型转换 和 引用类型转换:

数值类型转换:

Java 允许兼容的数值类型之间转换。小范围类型可以自动提升到大范围类型(如 int 自动提升为 long/double),称为宽化转换(widening conversion),这是安全的不会溢出。反之,大类型转换为小类型需要显式强制转换(如 (int)3.5 得到3),这是窄化转换,可能丢失信息,编译要求强转来表示你知晓风险。如果没有强转,编译报错。例如:

1
2
3
4
int i = 10;
long l = i; // OK,int自动提升为long
short s = i; // 编译错误,需强转,因为int->short可能溢出
short s2 = (short)i; // 强制转换

整型和浮点型之间转换也遵循此规则,double->int必须强转等。Java 不区分 const 类型,因此不存在const_cast。一切数值转换都是值拷贝。

引用类型转换(类/接口)

Java 只有单一继承,但有接口多实现,所以类型转换有三种情况:向上转型、向下转型、跨接口/类转型。

向上转型 (Upcasting)

子类对象赋给父类引用是隐式安全的,无需强转。例如 B extends A ,可以 A a = new B(); 。这是因为 B 是 A 的一种,父类引用能代表子类对象。这类似 C++的向上转换,可以隐式进行且安全。接口方面,类实现接口,实例赋给接口引用也可隐式转换:
List list = new ArrayList();

向下转型 (Downcasting)

父类引用如果实际指向子类对象,可强制转换为子类类型引用。

1
2
A a = new B();
B b = (B) a; // 需要强制转换

Java 在运行时会检查这个转换是否可能:如果 a 实际不是指向一个 B 类型对象,转换将抛出ClassCastException 。因此,Java 的向下转型相当于 C++ 的 dynamic_cast 行为——具有运行时类型检查,保证安全。开发者可以在强转前用 instanceof 检查:

1
2
3
if(a instanceof B) {
B b = (B) a;
}

这类似 C++ 中先 dynamic_cast 判空再用。其实 Java 的强制转型本质上包含了一个内部的instanceof 检查,不通过就抛异常。因此在正确逻辑中只要转型对象类型匹配就不会有问题。

向下转型在Java中必须手动写强转运算 (Type)obj ,编译器不会自动做,以防不安全。

不相关类型转换

Java 不允许把毫无继承关系的类型互相转换,比如想把 String 转换成StringBuilder 会编译错误。而 C++ 在编译层面也不允许不相关指针 static_cast,除非用reinterpret_cast (Java 没有这种武器)。Java 通过泛型提供编译期类型安全(但有类型擦除限制),通常不需要类似 reinterpret_cast 的机制。

接口与类转换

类实现接口,接口引用强转成实现类引用,或者反之,都算上下转型,原则一样。特别地,如果一个对象实现了多个接口,接口之间转型需要先转成实现类或公共父接口。例如:

1
2
3
4
5
interface X {}
interface Y {}
class C implements X, Y {}
X x = new C();
Y y = (Y) x; // 需要强转,编译允许但运行时检查实例
  1. 为什么编译器允许 (Y) x?
    当编译器看到 Y y = (Y) x; 这行代码时,它的思考过程是这样的:

“我知道变量 x 的类型是接口 X。”

“程序员想把它转成接口 Y。”

“X 和 Y 本身没有继承关系,它们是两个独立的接口。”

“但是,有没有可能存在一个类,它同时实现了 X 和 Y 两个接口呢?”

“答案是肯定的,比如例子中的 C 类,或者未来可能出现的任何一个 class D implements X, Y。”

因为存在这种可能性,编译器不能武断地认为这个转换是错误的。它无法在编译阶段预知 x 在运行时到底会指向哪个具体对象。所以,编译器选择信任程序员,允许这次编译,同时默默地埋下一个“运行时检查”的指令。

  1. 运行时检查做了什么?
    当 JVM 执行到 Y y = (Y) x; 这行代码时,它的操作就非常具体了:

获取真实对象:JVM 查看引用 x 当前在内存中指向的实际对象。在这个例子中,它指向的是一个 new C() 的实例。

检查继承/实现关系:JVM 会问一个问题:“这个 C 类的实例,是不是 Y 接口的一个实现?” (Does C implement Y?)。

做出裁决:

情况一(成功):JVM 检查 C 类的定义,发现 class C implements X, Y。答案是肯定的!C 确实实现了 Y 接口。因此,类型转换成功,程序继续执行,变量 y 会正确地指向那个 C 对象。

情况二(失败):这正是您问题的后半部分——“否则抛异常”。

数组转换

协变(Covariance)意味着,如果 Dog 是 Animal 的子类型,那么 Dog[] 也被看作是 Animal[] 的子类型。这允许我们将一个子类数组赋值给父类数组的引用。

Java数组是协变的, Sub[] 可以赋给 Base[] (因为 Sub extends Base),但这里存在运行时数组存储检查。Java 数组会记住元素实际类型,如果不匹配插入会抛 ArrayStoreException 。这一设计被认为不太安全(泛型没有采用协变数组策略)。
C++数组则无这层检查。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 1. 定义一个简单的继承体系
class Animal {
@Override
public String toString() {
return "An animal";
}
}

class Dog extends Animal {
@Override
public String toString() {
return "A Dog";
}
}

class Cat extends Animal {
@Override
public String toString() {
return "A Cat";
}
}

public class ArrayCovarianceExample {
public static void main(String[] args) {
// 2. 创建一个 Dog 数组
// 这个数组在内存中被“标记”为只能存放 Dog 或其子类的对象
Dog[] dogs = new Dog[3];
dogs[0] = new Dog();

// 3. 数组协变:将 Dog[] 赋值给 Animal[] 引用。编译通过!
// 因为 Dog[] 是 Animal[] 的子类型
Animal[] animals = dogs;
System.out.println("成功将 Dog[] 赋值给 Animal[] 引用。");

// 4. 尝试通过父类引用向数组中存入一个 Dog 对象。成功!
// 运行时检查:要存入的对象 (Dog) 是不是数组真实类型 (Dog) 的实例?是的。
animals[1] = new Dog();
System.out.println("成功存入一个 Dog 对象。");

// 5. 尝试通过父类引用向数组中存入一个 Cat 对象。编译通过,但运行时会失败!
System.out.println("准备尝试存入一个 Cat 对象...");
try {
// 编译器只知道 animals 是一个 Animal[],存入一个 Cat (也是 Animal) 在编译层面是合法的。
// 但在运行时,JVM 知道这个数组的真实类型是 Dog[]。
// 运行时检查:要存入的对象 (Cat) 是不是数组真实类型 (Dog) 的实例?不是!
// => 抛出 ArrayStoreException
animals[2] = new Cat();
} catch (ArrayStoreException e) {
System.err.println("捕获到异常!操作失败!");
e.printStackTrace(System.err);
}
}
}

泛型类型

Java 泛型采取了与数组完全相反的策略:不变性(Invariance)。List 不是 List 的子类型。这样做主要是为了在编译期就保证类型安全,避免 ArrayStoreException 这类运行时问题。其实现机制是类型擦除

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import java.util.ArrayList;
import java.util.List;

public class GenericsErasureExample {

public static void main(String[] args) {
// --- 2.1 类型擦除的影响 ---
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();

// 编译错误:因为泛型类型在运行时被擦除,JVM无法区分它们,所以 instanceof 不支持泛型类型参数
// if (stringList instanceof ArrayList<String>) {}
//stringList 和 integerList 在运行时的类都是 java.util.ArrayList,它们的泛型参数 <String> 和 <Integer> 已经被擦除了。

System.out.println("stringList 的运行时 class: " + stringList.getClass());
System.out.println("integerList 的运行时 class: " + integerList.getClass());
System.out.println("两者 class 是否相等: " + (stringList.getClass() == integerList.getClass()));

// --- 2.2 不安全的泛型转换 ---
System.out.println("\n--- 演示不安全的泛型转换 ---");
List<Integer> myInts = new ArrayList<>();
myInts.add(42);

// 这是“欺骗”编译器的方法,但在实践中非常危险!
List rawList = myInts; // 1. 赋值给一个原始类型 List,丢失了泛型信息
@SuppressWarnings("unchecked") // 2. 用 @SuppressWarnings 压制“未检查的转换”警告
List<String> myStrings = (List<String>) rawList; // 3. 将原始类型强转为 List<String>

System.out.println("成功将 List<Integer> 的引用“转换”为 List<String>。");

try {
// 4. 灾难发生点:当你试图从集合中取出元素并当作 String 使用时
// 这行代码会编译通过,因为编译器相信 myStrings 里就是 String
// 但在运行时,JVM 从集合里取出的是一个 Integer,尝试将它赋值给 String 变量时,类型不匹配
// => 抛出 ClassCastException
String firstElement = myStrings.get(0);
System.out.println("取出的元素是: " + firstElement); // 这行不会执行
} catch (ClassCastException e) {
System.err.println("捕获到异常!在试图使用元素时失败了!");
e.printStackTrace(System.err);
}

// --- 2.3 更安全的方式:传递 Class<T> 对象 ---
System.out.println("\n--- 演示更安全的处理方式 ---");
processElements(myInts, Integer.class);
processElements(myInts, String.class); // 演示检查失败的情况
}

/**
* 一个更健壮的泛型方法,它接收一个 Class<T> 对象来在运行时辅助进行类型检查
*/
public static <T> void processElements(List<?> list, Class<T> targetType) {
System.out.println("正在用类型 " + targetType.getSimpleName() + " 处理列表...");
for (Object item : list) {
// 使用 Class 对象进行运行时类型检查
if (targetType.isInstance(item)) {
T typedItem = targetType.cast(item); // 安全地进行转换
System.out.println(" 成功转换元素: " + typedItem);
} else {
System.out.println(" 元素 '" + item + "' 不是目标类型 " + targetType.getSimpleName());
}
}
}
}

3 重载

3.1 函数的重载

cpp

在同一作用域(同一个作用域或类)中,声明多个同名函数但参数类型或个数不同,它们即构成重载关系 。调用时编译器根据实参类型和个数选择匹配度最高的函数

  • 返回值类型,顶层 const (修饰返回值或值传递参数)不参与重载判别.底层 const(指针或引用参数指向常量)会视为签名不同。例如 void f(int*) vs void f(const int*) 是有效重载
  • 重载决议顺序 :编译器会寻找最佳匹配:首先完全匹配的非模板函数>完全匹配的模板函数>需要提升/转换的函数。如果有多个候选都同等匹配,则报“调用不明确”错误 。如果没有找到可行的,则报“无匹配函数”错误。
  • 类型转换与重载:编译器在匹配参数时,可考虑标准类型转换(如 int->double)或用户自定义转换(如构造函数或转换运算符)。如果多个重载都能通过不同转换匹配,会导致二义性。
    1
    2
    3
    4
    void foo(int a, int b) { cout<<1; }
    void foo(double a, double b) { cout<<2; }
    // void foo(int a, double b) { ... } 假设没有这个版本
    foo(1, 2.0);
    第三个调用 foo(1,2.0) ,第一个重载需要将2.0转换为int,第二个重载需要将1转换为double。两者都可匹配,各需一次转换,没有明显优劣,所以编译会报模糊错误

java

Java 也支持方法重载,与 C++ 规则类似:同一类中方法名相同但参数列表不同即可重载。

  • 返回类型不参与判定,必须有不同参数列表。否则编译报重复定义错误。
  • 重载选择在编译期完成,根据实参静态类型选择最合适的方法。这一点与C++相同。Java会应用自动转换来尝试匹配,例如实参 int 可匹配形参long(widening),实参实现接口可匹配接口类型参数等。如果多个重载都可匹配且没有一个更优,就编译错误。
  • 自动提升 & 拆装箱 & 可变参数:Java比C++多了装箱/拆箱和可变参数的概念。先找最具体匹配,再拓宽原始类型,再考虑装箱,再考虑可变参数。若歧义无法消除则报错。例如,有重载foo(Integer) 和 foo(int…) 两种,调用 foo(5) 会如何?5 是 int,可匹配 foo(int…)(可变参数,foo 方法可以接受零个或多个 int 类型的参数,是数组,一个方法最多只能有一个可变参数,可变参数必须是方法的最后一个参数)直接,也可通过装箱为 Integer 匹配 foo(Integer)。这时会调用 foo(Integer)
    1
    2
    3
    4
    5
    例如:
    void foo(int a, int b) { System.out.println(1); }
    void foo(double a, double b) { System.out.println(2); }
    // 调用:
    foo(1, 2.0);
    在Java中,1 可转为 double,2.0 是 double,匹配 foo(double,double);或者 2.0 转为 int (窄化,不允许自动发生)。所以只有 foo(double,double) 是可行重载,因此调用输出2。这个和C++的例子不同:Java不允许双->int的隐式转换,消除了二义性。
  • 重载解析不考虑运行时类型——这是重载与重写的重要区别:重载在编译期决定,重写(覆盖)在运行期决定。如果通过父类引用调用一个重载方法,首先根据引用的静态类型选择匹配的重载版本,然后在该版本中如有多态再动态绑定。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class A { 
    void foo(Object x) {
    System.out.println("A:Obj");
    } }
    class B extends A {
    void foo(String x) {
    System.out.println("B:Str");
    } }
    A a = new B();
    a.foo("hello");
    这里 a 是 A 类型引用, foo(“hello”) 编译时会选 A 类的 foo(Object)(因为只有那个存在,对A而言B的foo不可见),运行时对象实际是 B,但 B 没有覆盖 foo(Object)(它定义的是不同签名foo(String)),所以仍调用 A 的 foo(Object),输出 “A:Obj”。这例子说明Java重载不具备多态性(只能按静态类型选),而 C++ 情况类似,如果 B 定义额外重载而非覆盖虚函数,通过基类指针也调用基类版本。这再次强调:要实现多态,应使用覆盖而非重载。

对比

对比: Java 与 C++ 的函数重载概念几乎相同,区别主要在于:Java 自动类型转换规则略有不同(无隐式窄化,添加了装箱/拆箱),但本质是类似的匹配优先级、模糊错误等。Java由于无默认参数,往往需要定义多个重载来实现类似功能。C++可以用默认参数减少重载数(Java通过方法重载或使用可变参数实现)。C++模板函数也可与重载竞争,而Java泛型在编译后类型擦除,不参与重载选择(即重载方法只能靠参数数量或原始类型区分,泛型参数只是引用类型,对重载而言原始类型相同)。总的来说,开发者需避免让重载过于模糊,以免使用时不确定选哪一个。

3.2运算符重载

cpp

C++ 允许对大部分内置运算符进行重载,使其作用于用户自定义类型时执行特定操作。运算符重载本质上是定义特殊名字的函数。

  • 并非所有运算符都可重载:不能重载 . 成员访问、 :: 作用域、 ?: 三目运算符,以及 sizeof 、typeid 等特殊运算符 。大多数其他比如 + - * / % 等都能重载。也不能创造新运算符,只能重载已有的。
  • 重载必须保持原运算符的元数和优先级不变 。例如 + 是双目运算符,重载后也只能是双目,无法让它变成一元。重载不改变运算符优先级/结合性——这些是编译器静态决定的,与重载函数无关。

运算符重载的实现方式:

函数名形式为 operator@ ,其中 @ 替换为运算符符号。

重载为成员函数

Complex operator+(const Complex& rhs) const { ... }实现复数加法。
作为成员函数,左操作数是隐含的 this (必须是类类型),右操作数作为参数。单目运算符成员函数无参数(如 operator-() 一元负号),双目有一个参数 .

重载为全局函数

参数个数与运算符目数一致(如双目两个参数) 。非成员重载往往需要访问类的私有成员,这时可将该函数声明为类的友元

一般经验

赋值 = 、下标 [] 、函数调用 () 、成员访问箭头 -> 这些运算符必须定义为类的成员函数,因为它们需要特殊语义(C++规定这些只能成员重载) 。

前置++/– 通常成员实现,后置++/– 通过一个 int 哑参数与前置区分 例如 Integer operator++(int) 。

其他如算术、比较运算符可灵活选择成员或非成员。通常将左操作数需要隐式转换的情况做非成员更灵活,例如 operator<< 和 operator>> 通常实现为非成员,以允许左边是 ostream 。

比较运算符复用: 为减少重复代码,通常实现 operator== 和 operator< 等基本比较,然后在其他比较运算符中调用它们 。C++20 提供了三向比较运算符 <=> 可自动生成其他比较。

转换运算符:

类可以定义转换为其他类型的运算符,如 operator double() const { … } 用于将Complex转换为double 。这样可以支持隐式或显式将对象转换为目标类型。不过隐式转换可能导致二义性或意外行为,因此可考虑标记 explicit 来避免自动发生隐式转换(C++11起可对转换运算符用explicit)。

java

Java 不支持运算符重载。所有运算符的功能对自定义类型来说都是固定的。
只有一个例外: +运算符被语言内置地重载用于字符串拼接。当一个字符串与另一个字符串或其他类型用 + 相连时,会触发 StringBuilder 拼接过程。例如: “Hello “ + 5 会将5转换为字符串并连接,得到 “Hello5” 。这个功能并非通过用户定义,而是Java编译器遇到字符串+就自动转换处理。
对于自定义类型的相等比较,Java提供 .equals() 方法(所有类继承自 Object 默认提供 equals ,可覆盖)用于语义相等性判断;哈希值通过 .hashCode() ;排序比较通过实现 Comparable 接口的compareTo 方法等。这些不是通过运算符而是通过方法来实现。在Java中, == 对对象比较的是引用是否相同,不可重载改变。
对于类似复数相加、矩阵相乘这些,在Java中只能定义方法如 add() 、 multiply() ,用方法调用代替运算符。虽然稍显啰嗦,但Java设计者认为这换来代码的简洁统一和避免歧义。
然而,这也让Java代码在处理数学运算时不如C++简洁直观。例如C++可以写 c = a + b; 对于Complex,而Java必须 c = a.add(b); 。为了可读性,Java引入了Records和重写toString()等手段改善表现,但仍不及运算符重载直接。
另外,需要注意Java的 + 拼接字符串在编译后会转换成 StringBuilder 操作,效率较低时可以手动使用 StringBuilder 或 String.format 优化。

3.3 输入输出流的重载

cpp

ostream& operator<< (ostream& os, const T& obj);
istream& operator>> (istream& is, T& obj);
典型实现模式是非成员友元函数,以便访问对象私有数据并与流对象交互。约定这类重载应返回流引用本身,以支持链式操作 。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point {
int x,y;
// 友元声明
friend std::ostream& operator<<(std::ostream& os, const Point& p);
friend std::istream& operator>>(std::istream& is, Point& p);
};
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << '(' << p.x << ", " << p.y << ')';
return os;
}
std::istream& operator>>(std::istream& is, Point& p) {
char ch;
is >> ch >> p.x >> ch >> p.y >> ch; // 简单读取格式 "(x, y)"
return is;
}
1
2
3
Point a{1,2}, b;
std::cout << "Point a: " << a << std::endl;
std::cin >> b;

java

输出:使用 System.out.print() / println() 打印基本类型和对象(调用其 toString() )。或者使用 PrintWriter 、 BufferedWriter.write().print() 方法输出。Java 还提供了String.format Formatter 来格式化字符串然后输出,相当于 C++ 的 printf 风格。Java 8 也引入了流式 API(java.util.stream),但那是用于集合处理,不是I/O。
输入:使用 Scanner 辅助从 InputStream (如 System.in)读取基本类型和字符串,或用BufferedReader 逐行读文本,然后再解析。也可以使用DataInputStreamObjectInputStream读取二进制数据。总之,没有像C++那样直接 cin >> x 的语法糖,只能调用方法如scanner.nextInt()

如果想给自定义类型实现类似 << 的方便输出,Java的方式是覆盖 toString() 方法:当对象用于字符串环境或被 PrintStream打印时,会自动调用其 toString()。例如

1
2
3
4
5
class Point { int x,y;
public String toString() { return "(" + x + ", " + y + ")"; }
}
Point p = new Point(1,2);
System.out.println("Point p: " + p);

这会输出 Point p: (1, 2) ,因为 p 用 + 拼接字符串时 Java 调用了 p.toString()

4 模板 (Templates) 与泛型 (Generics)

4.1 namespace

cpp

命名空间用于将标识符划分在不同空间,避免全局命名冲突.
语法: namespace Name{ … } 来声明。使用时可以以 Name::identifier 访问,或通过 using namespace Name; 引入全部名称 。命名空间可以嵌套,可以在多个块中打开同名命名空间(追加内容)。
C++标准库本身就在 std 命名空间中。因此我们经常用 using namespace std; 或 std:: 限定来使用标准库功能。

1
2
3
4
5
6
7
8
namespace space1 { string name = "randomstar"; }
namespace space2 { string name = "ToyamaKasumi"; }
using namespace space2;
int main() {
cout << space1::name << endl; // 输出 space1 的 name
cout << name << endl; // 因为 using namespace space2,所以直接用 name 指 space2::name
return 0;
}

java

Java 没有与命名空间完全对等的概念,但包 (Package) 实现了类似功能
Java 用 packagecom.example.myapp;声明类所属的包,用 import com.example.myapp.ClassName; 引入别的包中的类以便使用短名。包名通常以公司域名反转开头,确保全球唯一性,从而避免命名冲突。
import com.foo.*; 导入 foo 包下所有类的短名,但不会导入子包;
import com.foo.Bar; 只导入指定类 Bar;
java 没有导入“所有”功能,也没有类似C++ using namespace直接把标识符引入当前作用域,如果冲突需全限定名。
例如,如果同时有 java.util.Date 和 java.sql.Date 两个类,import java.util. 和 import java.sql.同时导入,会导致 Date 类名冲突,此时必须写全称或只 import 一个。Java 不能像C++那样为包起别名或选择性导入函数。
由于Java没有全局函数,包主要用来隔离类名。内部原理上,包也决定访问权限:默认权限下,同包类可互访对方的无修饰成员。C++命名空间不涉及权限,仅仅是名字分类。

4.2 模板 (C++ Templates vs Java Generics)