后台私信作者即可获取MD源文件!
一、c++基本语言知识和框架
1.vscode的c++/c语言的环境配置
参考博客:VsCode安装和配置c/c++环境(超完整,小白专用)
2.头文件:
万能头文件
#include <bits/stdc++.h> //万能头文件
会了上面那一个下面都不用记
#include <iostream.h>//cin,cout文件
#include <algorihm.h>//算法函数文件
#include <math.h>//数学函数文件
#include <string.h>//字符串文件
#include<list>// 链表
#include<map>//图
#include<queue>//队列
#include<vector>//迭代器
#include<stack>//栈
注意有一些不可以加
.h
如果头文件报错可以尝试去掉.h
万能头文件的优缺点:
优点:
1、在竞赛中节约时间
2、减少了编写所有必要头文件的工作量
3、对于使用的每个函数,不用记住GNU C++的所有STL
缺点:
1、不属于GNU C++库的标准头文件,在部分情况下可能会失败
2、使用它将包含许多不必要的东西,并增加编译时间
3、这个头文件不是C++标准的一部分,因此是不可移植的,应该避免
4、编译器每次编译翻译单元时都必须实际读取和分析每个包含的头文件,应该避免
3.c++基础语法:(类比c语言快速记忆)
首先写一个c++程序:
#include <bits/stdc++.h>
using namespace std;
int main()
{
cout<<"Hello world!!!"<<endl;
return 0;
}
作为已经学习完c语言的我们来对比的看看那些是我们不会的东西
using namespace std
输入输出方式
回车
3.1.命名空间:namespace
假设这样一种情况,当一个班上有两个名叫 小明
的学生时,为了明确区分它们,我们在使用名字之外,不得不使用一些额外的信息,比如他们的家庭住址,或者他们父母的名字等等。
同样的情况也出现在 C++ 应用程序中。例如,您可能会写一个名为 add()
的函数,在另一个可用的库中也存在一个相同的函数 add()
。这样,编译器就无法判断您所使用的是哪一个 add()
函数。
因此,引入了命名空间这个概念,专门用于解决上面的问题,它可作为附加信息来区分不同库中相同名称的函数、类、变量等,使用了命名空间即定义了上下文。
本质上,命名空间就是定义了一个范围。
using 指令
使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。
e.g.
#include <iostream>
namespace core
{
int rand = 2;
int a=1,b=2;
int add(int a,int b)
{
return a+b;
}
}
using namespace core;
using namespace std;
int main()
{
cout<<add(a,b)<<endl;
}
这个时候我们可以发现我们在main函数里面并没有定义a,b变量但我们使用了这个语句:
using namespace core;
我么就可以使用上面 namespace core{}
中定义的函数和变量
注意 :在使用core 空间中定义的变量的时,还要使用标准空间 std
,因为其中定义输入输出cout
等函数,否则就会报错
E:\c++\c++3.cpp|16|error: ‘cout’ was not declared in this scope|
E:\c++\c++3.cpp|16|error: ‘endl’ was not declared in this scope|
因为这样他编辑器无法找到cout
函数
另一种使用命名空间变量的方法,使用 ::
运算符
using core::rand;
这样可以引用core空间的rand元素而不是把所有元素都引入了!可以避免额外消耗
小结:
using namespace std;
允许你在当前作用域中直接使用std
命名空间中的标识符。- 使用
using namespace std;
可以让你直接写endl
,而不需要写std::endl
3.2.c++中的输入输出
我们都知道在c语言里面我们常常使用:
int a;
scanf("%d",&a);
printf("%d",a);
来进行输入输出,但是在c++里面爽的来了~~
我们不需要记忆什么%d,%lf什么的我们只需要cin,cout.就可以了。
cin
– 标准输入流对象。 cout
– 标准输出流对象。
大白话就是:cin是和输入有关的对象,cout是和输出有关的对象。
然后上面在c语言中的那句话在c++中应该是
int a;
cin >> a; // 输入a
cout << a;// 输出a
注意,cin >> a >> b
等同于java中 int a = in.next; int b = in.nextInt;
即二者都会等待控制台的输入,但是先后顺序不同
在c语言中的一些转义符也可以在c++中使用,例如:
#include <iostream>
using namespace std;
int main()
{
cout<<"the\n";
}
上例等同于:
#include <iostream>
using namespace std;
int main()
{
cout<<"the"<<'\n';
cout<<"the"<<'\n';
}
这两个的输出结果都是一样的,也可以改成<<endl
,一般我们都用<<endl
来表示回车
cin和cout的使用避免了C中 %d
,%lf
等复杂的声明
特别注意:cout可以输出字符型数组,但其他的数组不行
字符数组是可以用cout方式输出的,因为数组名即代表首地址,数组是一块连续的内存空间,所以能直接输出。如果是其他类型的数组则输出的是一个地址。二者根本区别在于没有终止符,因此其他数组要是直接能cout就无法停止下来。
e.g.
int main() {
// 字符数组
char str[] = "Hello, World!"; // C 风格字符串
std::cout << str << std::endl; // 正确输出: Hello, World!
// 整型数组
int arr[] = {1, 2, 3, 4, 5};
std::cout << arr << std::endl; // 输出: 地址,比如 0x7ffee5b5d9a0
// 若要输出整型数组的内容,需要使用循环
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " "; // 输出: 1 2 3 4 5
}
std::cout << std::endl;
return 0;
}
e.g.两数加法:
#include <iostream>
using namespace std;
int main()
{
int a,b;
cin>>a>>b;
cout<<a+b;
}
一些c++的输入输出案例
#include<iostream>
using namespace std;
int main()
{
int a = 1;
float b = 2.1;
double c= 2.111;
char arr[10] = { 0 };
char d[] = "hello world";
cin >> arr;
cout << arr << endl;
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << d << endl;
return 0;
}
注意:
这里我们还要注意下cin的特点,它和C语言中的gets有些像,gets是遇到换行符停止,而cin则遇到空格,tab或者换行符\n
停止,如输入hello world
时,由于其被空格符分隔开来,所以cout出来结果为hello。
3.3.c++的缺省函数(默认参数)
先来一段代码:
#include<iostream>
using namespace std;
void func(int a = 0)
{
cout << a << endl;
}
int main()
{
func(5);
func();
return 0;
}
来看看这两个func()执行结果分别是什么?:
5
0
这就要说到缺省函数了,如果未传入某个变量的话,可以在定义函数的时写上
void func(int a = 0)
这样如果用户不传入变量的话,默认a=0
这就是所谓的缺省函数
半缺省参数
void func(int a, int b, int c = 2)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
注意:
- 半缺省参数必须从右往左依次给出,即赋值的函数必须放在最后
//错误示例
void func(int a, int b = 2, int c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
- 缺省参数不能在函数声明和定义中同时出现
#include <iostream>
// 函数声明,带有缺省参数
void func(int a, int b = 10);
void func(int a, int b = 20) { // 这里再次定义缺省参数,编译会出错
std::cout << "a: " << a << ", b: " << b << std::endl;
}
int main() {
func(5); // 调用时,b 会使用默认值 10
return 0;
}
原因:编译器就无法确定到底该用哪个缺省值。
- 缺省值必须是常量或者全局变量。
#include <iostream>
// 有效的缺省参数
void validFunc(int a, int b = 10) {
std::cout << "a: " << a << ", b: " << b << std::endl;
}
// 有效的缺省参数
const int globalValue = 20;
void func(int a, int b = globalValue); // globalValue 是全局常量
// 下面的函数将导致编译错误,因为使用了局部变量
void invalidFunc(int a, int b = localValue) { // localValue 是局部变量,错误
int localValue = 30;
}
//下面的函数将导致编译错误,因为getValue() 返回值不是常量
int getValue() {
return 40;
}
void func(int a, int b = getValue()) { // 错误:getValue() 返回值不是常量
}
int main() {
validFunc(5); // 输出 a: 5, b: 10
return 0;
}
注:
- C++ :支持在函数声明中使用默认参数
int a = 1
。- Java:不支持默认参数。如果需要类似的功能,通常通过重载方法来实现。
3.4.C++的函数重载(同java)
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 参数类型 或 参数顺序)必须不同,常用来处理实现功能类似数据类型不同的问题
这是在c语言中不支持的!!!
#include <iostream>
using namespace std;
int Add(int x, int y)
{
return x + y;
}
double Add(double x, double y)
{
return x + y;
}
int main()
{
cout << Add(0,1) << endl;//打印0+1的结果
cout << Add(1.1,2.2) << endl;//打印1.1+2.2的结果
return 0;
}
注意:若仅仅只有返回值不同,其他都相同,则不构成函数重载。例如:
short Add(short left, short right)
{
return left+right;
}
// 非法重载
int Add(short left, short right)
{
return left+right;
}
函数重载的使用可以解决不同类型的数据的传入问题
3.5.C++的引用(不同于指针):& 引用变量名(对象名) = 引用实体
;
使用引用时,并未像指针一样指向一个地址,进而修改该变量;而是给某个变量”起别名“。编译器不会为引用变量开辟内存空间,引用实体和引用变量名共享同一片空间。
e.g.
#include<iostream>
using namespace std;
int main()
{
int a = 2;
int &b = a; //相当于给a起了一个别名为b
cout << a << endl;//输出结果:2
cout << b << endl;//输出结果:2
b = 3; //改变b也就相当于改变了a
cout << b << endl;//输出结果:3
cout << a << endl;//输出结果:3
}
这里&b
相当于a变量的别名
注意
1.引用在定义时必须初始化及int &b=a;
2.一个变量可以有多个引用
3.引用一旦引用了一个实体,就不能再引用其他实体
3.的错误示例:
#include <iostream>
void changeReference(int& ref) {
// ref 现在是 a 的别名
ref = 20; // 修改 a 的值
}
int main() {
int a = 10;
int& ref = a; // ref 绑定到 a
std::cout << "Before change: a = " << a << ", ref = " << ref << std::endl;
changeReference(ref); // 通过引用修改 a 的值
std::cout << "After change: a = " << a << ", ref = " << ref << std::endl;
// 尝试重新绑定引用(这是不允许的)
int b = 30;
// ref = b; // 错误:无法将引用重新绑定到其他对象
return 0;
}
4.vector容器的使用(相当于数组)
见博客
[vector的简单使用](https://blog.csdn.net/xiaopikadi/article/details/108991180?ops_request_misc=%7B%22request%5Fid%22%3A%22165839381116782350834928%22%2C%22scm%22%3A%2220140713.130102334…%22%7D&request_id=165839381116782350834928&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-1-108991180-null-null.142v33new_blog_pos_by_title,185v2control&utm_term= vector<>&spm=1018.2226.3001.4187)
5.运算相关(待补充)
6.C++中的动态内存(new与delete)
C++ 程序中的内存分为两个部分:
- **栈:**在函数内部声明的所有变量都将占用栈内存。
- **堆:**这是程序中未使用的内存,在程序运行时可用于动态分配内存。
很多时候,我们无法提前预知需要多少内存来存储某个定义变量中的特定信息,所需内存的大小需要在运行时才能确定。
在 C++ 中,可以使用 new
运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。
注意,返回地址说明在c++中只能使用指针接收new的对象
new
运算符用于在计算机开辟一段新的空间,但与一般的声明不同的是,new开辟的空间在堆上,而一般声明的变量存放在栈上。
如果您不再需要动态分配的内存空间,可以使用 delete 运算符,删除之前由 new
运算符分配的内存。
-
C++ 允许通过指针和引用来创建和管理对象。可以选择在栈上或堆上创建对象。
例如:
Person p1; // 在栈上创建对象 Person* p2 = new Person(); // 在堆上创建对象 // 下面的用法是错误的! 因为返回值是地址,必须用指针接收 // Person p3 = new Person();
-
在 Java 中,所有对象都是通过
new
关键字在堆上创建的,且不需要手动管理内存。例如:
Person p1 = new Person(); // 在堆上创建对象
malloc() 函数在 C 语言中就出现了,在 C++ 中仍然存在,但建议尽量不要使用 malloc() 函数。new 与 malloc() 函数相比,其主要的优点是,new 不只是分配了内存,它还创建了对象。
特别地,在创建数组时,我们仍然可以用new
运算符
char* pvalue = new char[20]; // 为变量请求内存
二、面向对象编程
C++面向对象编程的特性:封装、继承、多态。
要访问一个类内对象时,何时使用->
和 .
- 使用
.
:当你有一个对象的实例时。 - 使用
->
:当你有一个指向对象的指针时。
这种区分是因为 .
运算符直接作用于对象,而 ->
运算符则是通过指针间接访问对象的成员
1.C++中 成员函数的定义位置
C++ 和 Java 在类的成员函数定义方面有一些重要的区别。以下是一些关键的不同点,以及相应的示例来说明这些差异。
- C++ :成员函数可以在类内部或外部定义。内部定义的函数被视为内联函数,外部定义的函数需要使用作用域解析运算符
::
。 - Java:所有成员函数都必须在类的内部定义,不能在外部定义。
C++ 示例
#include <iostream>
class Box {
public:
double length;
double breadth;
double height;
// 成员函数在类内部定义
double getVolume() {
return length * breadth * height;
}
// 成员函数在类外部定义
void setDimensions(double len, double bre, double hei);
};
// 外部定义的函数需要使用作用域解析运算符 ::
void Box::setDimensions(double len, double bre, double hei) {
length = len;
breadth = bre;
height = hei;
}
int main() {
Box box;
box.setDimensions(3.0, 4.0, 5.0);
std::cout << "Volume: " << box.getVolume() << std::endl; // 输出: Volume: 60
return 0;
}
Java 示例
class Box {
double length;
double breadth;
double height;
// 所有成员函数必须在类内部定义
double getVolume() {
return length * breadth * height;
}
void setDimensions(double len, double bre, double hei) {
length = len;
breadth = bre;
height = hei;
}
public static void main(String[] args) {
Box box = new Box();
box.setDimensions(3.0, 4.0, 5.0);
System.out.println("Volume: " + box.getVolume()); // 输出: Volume: 60
}
}
2.C++中的类访问修饰符
- C++ :默认情况下,类的成员是私有的(
private
),但结构体的成员是公共的(public
)。可以使用public
、private
和protected
访问修饰符。 - Java:所有类的成员默认是包私有的(package-private),可以使用
public
、private
和protected
访问修饰符。
类访问修饰符
-
private 成员只能被本类成员(类内)和友元访问,不能被派生类(子类)访问;
-
protected 成员可以被派生类(子类)访问。
注意:在Java中,类可以被组织到包(package)中,而
protected
访问修饰符允许同一个包中的类和子类访问受保护的成员。c++中没有包的概念,因此只允许被子类访问
c++示例
#include <iostream>
using namespace std;
// 定义一个类
class MyClass {
private:
int privateVar; // 默认是 private
// :表示处理化列表,类似java的构造函数
public:
MyClass(int val) : privateVar(val) { // 结构体函数 }
void show() {
cout << "Private Variable: " << privateVar << endl;
}
};
// 定义一个结构体
struct MyStruct {
int publicVar; // 默认是 public
MyStruct(int val) : publicVar(val) {}
};
int main() {
MyClass obj1(10);
obj1.show(); // 输出10
// obj1.privateVar = 20; // 错误:privateVar 是私有的
MyStruct obj2(20);
cout << "Public Variable: " << obj2.publicVar << endl; // 可以直接访问
return 0;
}
在 C++ 中,
MyClass(int val) : privateVar(val) {}
中的冒号:
是初始化列表(initializer list)的语法,用于在构造函数中初始化类的成员变量。初始化列表允许你在构造函数体执行之前初始化成员变量,这样可以提高效率,尤其是对于复杂类型的成员变量。
java示例
class MyClass {
private int privateVar; // 私有成员
public MyClass(int val) {
privateVar = val;
}
public void show() {
System.out.println("Private Variable: " + privateVar);
}
}
public class Main {
public static void main(String[] args) {
MyClass obj1 = new MyClass(10);
obj1.show();
// obj1.privateVar = 20; // 错误:privateVar 是私有的
}
}
3.C++中类的成员的默认访问权限
在 C++ 中,类的成员的默认访问权限取决于定义的上下文:
- 类(class) :如果你使用
class
关键字定义一个类,那么类的成员(变量和方法)默认是 私有的 (private
)。 - 结构体(struct) :如果你使用
struct
关键字定义一个结构体,那么结构体的成员默认是 公共的 (public
)。
在java的类中,默认是包私有(package-private,仅仅包能访问),而不是protected(包和子类都可以访问)
e.g.
// 类的默认权限是private
class MyClass {
int privateVar; // 默认是 private
public:
void setVar(int val) {
privateVar = val; // 可以访问 privateVar
}
int getVar() {
return privateVar; // 可以访问 privateVar
}
};
int main() {
MyClass obj;
// obj.privateVar = 10; // 错误:无法访问 privateVar
obj.setVar(10); // 通过公共方法设置值
return 0;
}
// 结构体的默认权限是public
struct MyStruct {
int publicVar; // 默认是 public
};
int main() {
MyStruct obj;
obj.publicVar = 10; // 可以直接访问 publicVar
return 0;
}
4.C++中的构造函数和析构函数
构造函数
①无参构造函数:定义类时,无参构造函数自动生成
②有参构造函数:带有参数,在创建对象时就会给对象赋初始值
e.g.
#include <iostream>
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是无参构造函数
Line(double len); // 这是有参构造函数
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line(void){
cout << "Object is being created" << endl;
}
Line::Line( double len){
cout << "Object is being created, length = " << len << endl;
length = len;
}
void Line::setLength( double len ){
length = len;
}
double Line::getLength( void ){
return length;
}
// 程序的主函数
int main( ){
Line line(10.0);
// 获取默认设置的长度
cout << "Length of line : " << line.getLength() <<endl;
// 再次设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}
使用初始化列表来初始化字段
格式:构造函数名(可选参数) : 接受参数(传入参数)
使用初始化列表来初始化字段:
Line::Line( double len): length(len)
{
cout << "Object is being created, length = " << len << endl;
}
上面的语法等同于如下语法:
Line::Line( double len)
{
length = len;
cout << "Object is being created, length = " << len << endl;
}
假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示:
C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
/*
等同于
X = a;
Y = b;
Z = c;
*/
....
}
析构函数
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
java中有垃圾回收机制,无需关心,故无析构函数
-
析构函数也是一个特殊的成员函数,在对象生命周期结束时被调用,用于释放资源(如内存、文件句柄等)。
-
析构函数的名称是类名前加一个波浪号(~),没有参数和返回值。
-
每个类只能有一个析构函数。
-
示例:
#include <iostream> using namespace std; class Line { public: void setLength( double len ); double getLength( void ); Line(); // 这是构造函数声明 ~Line(); // 这是析构函数声明 private: double length; }; // 成员函数定义,包括构造函数 Line::Line(void) { cout << "Object is being created" << endl; } Line::~Line(void) { cout << "Object is being deleted" << endl; } void Line::setLength( double len ) { length = len; } double Line::getLength( void ) { return length; } // 程序的主函数 int main( ) { Line line; // 设置长度 line.setLength(6.0); cout << "Length of line : " << line.getLength() <<endl; return 0; }
执行结果
Object is being created
Length of line : 6
Object is being deleted
5.C++中的拷贝构造函数
拷贝构造函数也是构造函数的一种,其名称和类名一致,且没有返回值
其引入是为了让对象能够像普通变量一样被赋值
e.g.
Person p1 = new Person("张三", 18);
// 调用了拷贝构造函数
Person p2 = p1;
定义:
拷贝构造函数只接收一个参数,就是同类名的一个引用
声明:类名 (const 类名& 新对象);
实现:类名::类名 (const 类名& 新对象) { 将新对象中的各个属性赋值... }
当然,更常用的是直接使用初始化列表实现
类名::类名 (const 类名& 新对象) : 原变量1(新对象.新变量1),原变量2(新对象.新变量2)...
e.g.
Person::Person(const Person& p) : name(p.name), age(p.age){
其他处理...
}
注:拷贝构造函数默认已实现,无需书写
6.C++中的继承与类访问修饰符
子类继承父类 → 派生类继承基类
定义语法
class derived-class: access-specifier base-class
其中,访问修饰符 access-specifier
是 public、protected 或 private 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。
e.g.
// 基类
class Animal {
// eat() 函数
// sleep() 函数
};
//派生类
class Dog : public Animal {
// bark() 函数
};
继承中的类访问修饰符特点
有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。
- 1.**public 继承:**基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private
- 2.**protected 继承:**基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private
- 3.**private 继承:**基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private
7.C++友元函数、友元类、重载运算符
友元函数
目的:让非成员函数去访问私有的成员
定义:在类内部用friend关键字声明一个函数,这个函数称为友元函数,可以在外边访问类内部私有变量
friend 函数返回值 函数名(参数);
e.g.
class Box{
public:
Box(double w): width(w){} // 有参构造函数
private:
double wIdth;
// 友元函数定义
friend void printWidth(Box b);
};
void printWidth(Box b){
// 如不用友元函数声明,则会报错,因为width为私有成员变量
cout << "width of box: " << b.width << endl;
}
int main(){
Box b1(10.2);
printWidth(b1); // 在声明友元函数后,可以正确输出10.2
return 0;
}
友元类
与函数同理,我们可以将一个类声明为友元类,这个友元类便可以访问本类的所有私有变量和方法
e.g.(续上)
class B{
private:
int data;
public:
B(int d): data(d){}
// 声明友元类
friend class Box;
void printBox(Box box){
cout << box.width << " :" << data << endl;
}
}
int main(){
Box b1(10.2);
B b(100);
b.printBox(b1); // 在声明友元函数后,可以正确输出10.2 : 100
return 0;
}
优点:
在实现类之间数据时,减少系统开销,提高效率
缺点:
破坏了封装机制,尽量不使用
重载运算符
在C++中,运算符重载是允许开发者为自定义类型(如类)定义运算符的行为。
通过运算符重载,可以使自定义类型的实例使用常规运算符(如 +
, -
, *
, ==
等)时表现得像内置类型一样。
e.g.在java中,大数类BigInteger
之间的加法,只能通过内置函数实现,不能通过常规运算符
BigInteger b1 = new BigInteger(2);
BigInteger b2 = new BigInteger(5);
BigInteger b3;
b3 = b1 + b2; // 报错
b3 = b1.add(b2); // 必须使用内置的函数完成大数的数学运算
而在c++中,我们提供了运算符重载,可以对类自定义运算符的实现
**定义:**重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
Box operator+(const Box&);
上面的+
运算符可以换成其他需要重载的运算符。
在 C++ 中,运算符重载可以通过成员函数或非成员函数来实现,对于+运算符的重载,成员函数重载只需要一个参数,而非成员函数重载需要两个参数。
-
- 成员函数形式(以
+
运算符的重载为例)
当运算符被定义为类的成员函数时,通常只需要一个参数。这个参数是运算符左侧的对象,
this
指针指向调用该运算符的对象。举个例子,假设我们有一个Box
类,重载+
运算符如下:class Box { public: int length; int width; int height; Box operator+(const Box& other) { Box result; result.length = length + other.length; result.width = width + other.width; result.height = height + other.height; return result; } };
此例中,
operator+
是Box
类的成员函数,因此它只需要一个参数(other
),而调用该运算符的对象 (this
) 是隐式传递的。 - 成员函数形式(以
-
- 非成员函数形式(以
+
运算符的重载为例)
当运算符被定义为非成员函数时,通常需要两个参数,因为该函数没有隐式的上下文。它需要显式地知道两个操作数。继续以上面的例子,如果我们将
+
运算符定义为非成员函数,代码如下:class Box { public: int length; int width; int height; Box(int l, int w, int h) : length(l), width(w), height(h) {} }; // 非成员函数 Box operator+(const Box& b1, const Box& b2) { Box result; result.length = b1.length + b2.length; result.width = b1.width + b2.width; result.height = b1.height + b2.height; return result; }
在这个例子中,
operator+
需要两个参数(b1
和b2
),因为它不属于Box
类,因此没有隐式的this
指针。 - 非成员函数形式(以
选择成员函数还是非成员函数
- 成员函数 : 通常用于需要访问类的私有成员或需要自然地与类的实例进行操作的运算符。适合于一元运算符(如
++
、--
)或当左侧操作数是类的实例时。- 非成员函数 : 通常用于需要处理两个不同类型的操作数(例如,一个是类的实例,另一个是基本数据类型),或者当左侧操作数不是类的实例时。也适合于二元运算符(如
+
、-
、*
等),特别是当需要支持对称性时。
可重载运算符
双目算术运算符 | + (加),-(减),*(乘),/(除),% (取模) |
---|---|
关系运算符 | ==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于) |
逻辑运算符 | ||(逻辑或),&&(逻辑与),!(逻辑非) |
单目运算符 | + (正),-(负),*(指针),&(取地址) |
自增自减运算符 | ++(自增),–(自减) |
位运算符 | | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) |
赋值运算符 | =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>= |
空间申请与释放 | new, delete, new[ ] , delete[] |
其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
不可重载运算符
.
:成员访问运算符.
,->
:成员指针访问运算符::
:域运算符sizeof
:长度运算符?:
:条件运算符#
: 预处理符号
用友元函数重载运算符
对于许多运算符的重载,难免要访问类内部的私有成员,因此需要用friend
关键字修饰运算符重载函数,以使得其得以访问私有成员。
典例:见下面输入输出运算符的重载
重载输入输出运算符
class Distance{
private:
int feet; // 0 到无穷
int inches; // 0 到 12
public:
// 所需的构造函数
Distance(){
feet = 0;
inches = 0;
}
Distance(int f, int i){
feet = f;
inches = i;
}
// 重载输出运算符,固定语法
friend ostream &operator<<( ostream &output, const Distance &D ){
output << "F : " << D.feet << " I : " << D.inches;
return output;
}
// 重载输入运算符,固定语法
friend istream &operator>>( istream &input, Distance &D ){
input >> D.feet >> D.inches;
return input;
}
};
int main(){
Distance D1(11, 10), D2(5, 11), D3;
cout << "Enter the value of object : " << endl;
cin >> D3;
cout << "First Distance : " << D1 << endl;
cout << "Second Distance :" << D2 << endl;
cout << "Third Distance :" << D3 << endl;
return 0;
}
8.C++中的多态
多态的实现方式
- C++ :C++ 支持两种类型的多态:
- 静态多态(编译时多态) :通过函数重载和运算符重载实现。
- 动态多态(运行时多态) :通过虚函数(使用
virtual
关键字)实现。基类的指针或引用可以指向派生类的对象,调用虚函数时会根据对象的实际类型来决定调用哪个版本的函数。- Java:Java 主要通过动态多态来实现,所有非静态的方法都是虚拟的(即默认是虚函数)。Java 不支持静态多态(如运算符重载),只有通过方法重载实现。
关键字
- C++ :
- 使用
virtual
关键字来声明虚函数。- 使用
override
和final
关键字来控制函数的重写行为(C++11 及以后)。- Java:
- 使用
@Override
注解来表示一个方法是重写父类的方法。- 使用
final
关键字来禁止方法的重写。抽象类和接口
- C++ :可以通过纯虚函数(使用
= 0
)来定义抽象类。C++ 不支持接口的概念,但可以通过纯虚函数的组合来实现类似的功能。- Java:Java 有专门的接口(interface)来定义一组方法,类可以实现多个接口。接口中的方法默认都是抽象的(Java 8 及以后可以有默认方法)。
静态多态:默认继承
#include <iostream>
using namespace std;
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0){
width = a;
height = b;
}
int area(){
cout << "Parent class area :" <<endl;
return 0;
}
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
// 程序的主函数
int main( )
{
Shape *shape;
Rectangle rec(10,7);
// 存储矩形的地址
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();
return 0;
}
输出结果:
Parent class area :
导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。
动态多态:使用vitual
关键字,修饰的函数称为虚函数
在 Shape 类中,area() 的声明前放置关键字 virtual
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0){
width = a;
height = b;
}
virtual int area(){
cout << "Parent class area :" <<endl;
return 0;
}
};
修改后,执行结果:
Rectangle class area :
此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。
由于Area类的每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。
使用vitual关键字后,c++实现了java中的多态方式
虚函数
虚函数是在基类中使用关键字virtual
声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
纯虚函数,即java中的抽象方法
若要在基类中定义虚函数,以便在派生类中**(强制)重新定义(重写)该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数,即为某个虚函数赋值为0 (=0
)**。
我们可以把基类中的虚函数 area() 改写如下:
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// 纯虚函数, 即java中的抽象方法
virtual int area() = 0;
};
= 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数。
C++的抽象类(接口)
在C++中,如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。
c++中没有接口的概念,但可以模拟接口的实现,即不设置成员变量
-
定义一个函数为虚函数,不代表函数为不被实现的函数。(虚函数可以有实现,或在子类被重写)
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。(即动态绑定)class Base { public: virtual void show() { // 虚函数,有实现 std::cout << "Base class show function called." << std::endl; } }; class Derived : public Base { public: void show() override { // 重写虚函数 std::cout << "Derived class show function called." << std::endl; } }; int main() { Base* b; // 基类指针 Derived d; // 派生类对象 b = &d; // 基类指针指向派生类对象 b->show(); // 调用派生类的 show(),输出 "Derived class show function called." return 0; }
上例也是java中多态常见案例:父类调用子类方法 的C++实现
-
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数(类似java抽象类)
9.C++的函数中,传入引用还是传入类?
在 C++ 中,决定是以引用(&
)的方式传入类对象,还是直接传入类对象,取决于几个因素,包括类的大小、性能要求以及是否需要修改传入的对象。
- 传入类的大小 :
- 如果类的对象比较大(例如,包含大量数据成员或复杂的结构),直接传入对象会导致复制开销。此时,使用引用(
&
)传入对象可以避免不必要的复制,提高性能。 - 如果类的对象很小(例如,只有几个基本数据类型的成员),直接传入对象的性能影响可以忽略不计。
- 如果类的对象比较大(例如,包含大量数据成员或复杂的结构),直接传入对象会导致复制开销。此时,使用引用(
- 是否需要修改对象 :
- 如果你希望在函数中修改传入的对象,则应该使用非
const
引用(&
)。这样,函数可以直接修改原始对象。 - 如果不使用引用,而是类似java一样直接传入对象,那么只会生成副本,不会修改原始对象(在 Java 中,所有对象都是通过引用传递的,这意味着当你将一个对象传递给方法时,传递的是对象的引用,而不是对象本身的副本)
- 如果你不希望在函数内修改对象,且希望避免复制开销,可以使用
const
引用(const T&
),这样可以在保护对象不被修改的同时,避免复制。
- 如果你希望在函数中修改传入的对象,则应该使用非
class MyClass {
public:
int data;
MyClass(int d) : data(d) {}
};
// 直接传入对象
void func(MyClass obj) {
// obj 是 obj 的一个副本
obj.data = 10; // 只会修改副本,不会影响原始对象
}
// 通过引用传入对象
void funcByRef(MyClass& obj) {
obj.data = 10; // 直接修改原始对象
}
// 通过 const 引用传入对象
void funcByConstRef(const MyClass& obj) {
// obj.data = 10; // 错误,无法修改 const 引用
std::cout << obj.data; // 可以读取,但不能修改
}