c++学习记录:类的封装,继承和多态

从一段 C++ 图形代码理解面向对象:抽象类、继承、多态、clone 和对象切片

最近我学习 C++ 面向对象时,写了一段图形类代码。代码大概是这样:

#include<bits/stdc++.h>
using namespace std;
class graphic
{
public:
	virtual double get_area() const=0;
	virtual double get_perimeter() const=0;
	virtual graphic *clone() const=0;
};
class circle:public graphic
{
public:
	circle(double r)
	{
		this->r=r;
	}
	double get_area() const override
	{
		return r*r*pi;
	}
	double get_perimeter() const override
	{
		return r*2.0*pi;
	}
	graphic *clone() const override
	{
		return new circle(*this);
	}
private:
	double r;
	const double pi{3.14};
};
class square:public graphic
{
public:
	square(double r)
	{
		this->r=r;
	}
	double get_area() const override
	{
		return r*r;
	}
	double get_perimeter() const override
	{
		return r*4.0;
	}
	graphic *clone() const override
	{
		return new square(*this);
	}
private:
	double r;
};
class graphic_list
{
public:
	void add(const graphic &g)
	{
		graphics.emplace_back(g.clone());
	}
	void print_all() const
	{
		for(auto *u:graphics)
		{
			cout<<"perimeter:"<<u->get_perimeter()<<endl;
			cout<<"area:"<<u->get_area()<<endl;
			cout<<endl;
		}
	}
private:
	vector<graphic *> graphics;
};
int main()
{
	graphic_list my_list;
	my_list.add(circle{4});
	my_list.add(square{5});
	my_list.print_all();
	return 0;
}

原版代码是这样的:

#include<bits/stdc++.h>
using namespace std;
class graphic
{
	public:
		virtual double get_area() const =0;
		virtual double get_perimeter() const =0;
		virtual graphic *clone() const =0;//why *
};
class circle : public graphic
{
	public:
		circle(double r)
		{
			this->r=r;
		}
		double get_area() const override
		{
			return r*r*pi;
			
		}
		double get_perimeter() const override
		{
			return r*2.0*pi;
		}
		graphic *clone() const override
		{
			return new circle(*this);
		}
	private:
		double r;
		const double pi{3.14};
};
class square : public graphic
{
	public:
		square(double r)
		{
			this->r=r;
		}
		double get_area() const override
		{
			return r*r;
		}
		double get_perimeter() const override
		{
			return r*4.0;
		}
		graphic *clone() const override
		{
//			return new graphic(*this); why output err?
			return new square(*this);
		}
	private:
		double r;
};
class graphic_list
{
	public:
		void add(const graphic &g)//为什么要加const和&
		{
			graphics.emplace_back(g.clone());
		}
		void print_all() const
		{
			for(auto *u:graphics)
			{
				cout<<"perimeter:"<<(*u).get_perimeter()<<endl;
				cout<<"area:"<<(*u).get_area()<<endl;
				cout<<endl;
				
/*
				for(auto u:graphics)
				{  
					cout<<"perimeter:"<<u.get_perimeter()<<endl;
					cout<<"area:"<<u.get_area()<<endl;
					cout<<endl;
				}
				why this error
*/				
			}

		}
	private:
		vector<graphic*> graphics;//why *
};
int main()
{
	graphic_list my_list;
	my_list.add(circle(4));
	my_list.add(square(2));	
	my_list.add(circle(4));
	my_list.print_all();	
	return 0;
}

这段代码表面上是在算圆和正方形的面积、周长,但实际上已经包含了 C++ 面向对象里几个关键概念:

  • 对象
  • 抽象类
  • 纯虚函数
  • 继承
  • 多态
  • 父类指针指向子类对象
  • 对象切片
  • clone 多态复制
  • 引用传参
  • 指针容器

下面按我学习时遇到的问题逐个总结。


1. 什么是对象?

对象可以简单理解成:

一份具体的数据 + 能操作这份数据的函数。

比如:

circle c{4};

这里 circle 是类,c 才是对象。

c 这个对象内部可以理解成:

circle对象
{
	r=4;
	pi=3.14;
	get_area();
	get_perimeter();
}

严格来说,成员函数不是真的每个对象单独存一份,但从使用角度看,可以先理解成:对象有自己的数据,并且可以调用属于这个类的函数。

类是模板,对象是根据模板创建出来的具体实体。

比如:

circle a{4};
circle b{10};

ab 都是 circle 对象,但它们的数据不同:

a.r=4
b.r=10

2. 成员变量为什么可以写在类的最后?

比如:

class circle:public graphic
{
public:
	double get_area() const override
	{
		return r*r*pi;
	}
private:
	double r;
	const double pi{3.14};
};

虽然 rpi 写在后面,但前面的成员函数依然可以使用它们。

原因是:编译器会先看完整个类的定义
类里的成员函数可以访问同一个类中的成员变量,不要求成员变量一定写在成员函数前面。

这和普通函数里的变量不一样。普通函数里变量通常需要先定义再使用。


3. this->r=r; 里面的 this-> 是什么?

构造函数里有:

circle(double r)
{
	this->r=r;
}

这里左边和右边都叫 r,但它们不是同一个东西。

  • this->r:当前对象的成员变量
  • r:构造函数传进来的参数

所以:

this->r=r;

意思是:

当前对象的r = 参数r;

如果写成:

r=r;

那就会变成参数自己给自己赋值,成员变量没有被正确赋值。

如果参数换个名字,就可以不写 this->

circle(double x)
{
	r=x;
}

更推荐的写法是初始化列表:

circle(double r):r(r)
{
}

这里:

:r(r)

前面的 r 是成员变量,后面的 r 是参数。


4. const double pi{3.14}; 为什么这样初始化?

const double pi{3.14};

这是类内成员默认初始化。

它表示:每个 circle 对象里的 pi 默认就是 3.14

也可以写成:

const double pi=3.14;

花括号 {} 是 C++ 里比较统一的初始化方式,也能防止某些隐式类型转换问题。

比如:

int x=3.14;

这可以编译,但会把 3.14 截断成 3

而:

int x{3.14};

一般会报错,因为它阻止了这种有风险的窄化转换。

另外,piconst,所以它必须在初始化阶段确定值,不能等进入构造函数后再赋值。


5. 什么是抽象类?

父类 graphic 是这样写的:

class graphic
{
public:
	virtual double get_area() const=0;
	virtual double get_perimeter() const=0;
	virtual graphic *clone() const=0;
};

这里有三个纯虚函数:

virtual double get_area() const=0;
virtual double get_perimeter() const=0;
virtual graphic *clone() const=0;

只要一个类里面有至少一个纯虚函数,这个类就是抽象类。

=0 不是返回 0,而是表示:

父类不实现这个函数,要求子类必须实现。

所以 graphic 的含义是:

所有图形都应该能算面积
所有图形都应该能算周长
所有图形都应该能复制自己
但 graphic 自己不知道具体怎么算

因此,graphic 不能创建对象:

graphic g;

这是错误的。

因为编译器不知道:

g.get_area()
g.get_perimeter()
g.clone()

到底应该执行什么。


6. 为什么 circlesquare 可以创建对象?

circle 继承了 graphic

class circle:public graphic

并且实现了父类要求的所有纯虚函数:

double get_area() const override
double get_perimeter() const override
graphic *clone() const override

所以 circle 不再是抽象类,可以创建对象:

circle c{4};

square 也是一样。

规则是:

父类有纯虚函数 -> 抽象类 -> 不能创建对象
子类实现所有纯虚函数 -> 具体类 -> 可以创建对象

如果 circle 少实现一个函数,比如没写 clone(),那么 circle 仍然是抽象类,也不能创建对象。


7. override 是什么?

比如:

double get_area() const override
{
	return r*r*pi;
}

override 最好翻译成 重写

它的作用是告诉编译器:

我这里是在重写父类的虚函数,请帮我检查

父类是:

virtual double get_area() const=0;

子类必须签名一致:

double get_area() const override

注意 const 不能少。

如果写成:

double get_area() override

就不是同一个函数,因为父类版本后面有 const,子类版本没有。
这时 override 会让编译器报错,帮我们提前发现问题。

所以可以记成:

virtual  -> 父类说这个函数允许被重写
override -> 子类说我正在重写,请检查

8. 为什么 add 要写成 const graphic &g

代码是:

void add(const graphic &g)
{
	graphics.emplace_back(g.clone());
}

这里有两个重点:&const

为什么要加 &

如果写成:

void add(graphic g)

这是错的。

因为按值传参需要创建一份新的 graphic 对象。
graphic 是抽象类,不能创建对象。

而且即使 graphic 不是抽象类,也会发生对象切片。

所以要写引用:

void add(const graphic &g)

这样不会复制对象,只是让 g 引用传进来的真实对象。

比如:

my_list.add(circle{4});

此时:

g 引用的是一个 circle 对象

不是创建了一个新的 graphic 对象。

为什么要加 const

const graphic &g

表示:

我只是借用这个图形,不会修改它

这样更安全。

同时因为父类里的 clone() 也写了 const

virtual graphic *clone() const=0;

所以 const graphic& 也能调用:

g.clone()

9. 什么是对象切片?

对象切片是理解这段代码的关键。

假设有:

class graphic
{
};

class circle:public graphic
{
private:
	double r;
};

一个 circle 对象可以理解成:

circle对象 =
[
	graphic父类部分
	circle自己的r
]

如果按值赋给父类对象:

circle c{4};
graphic g=c;

这时会发生对象切片。

也就是:

circle{graphic部分 + r}

被切成:

graphic{graphic部分}

circle 自己的 r 丢了。

所以多态代码里一般不能这样写:

void add(graphic g)

也不能这样保存:

vector<graphic> graphics;

因为它们都会试图按值保存父类对象,要么因为抽象类不能创建而报错,要么发生对象切片。

正确做法是用引用或指针:

void add(const graphic &g)
vector<graphic *> graphics;

10. 为什么 vector<graphic *> graphics; 要存指针?

因为我们想在同一个容器里保存不同类型的图形:

circle
square
triangle

如果写:

vector<graphic> graphics;

不行。

原因有两个:

第一,graphic 是抽象类,不能创建对象。
第二,即使它不是抽象类,保存 circlesquare 时也会对象切片。

所以要写:

vector<graphic *> graphics;

它保存的是对象地址,不是对象本体。

可以理解成:

graphics[0] ---> circle对象
graphics[1] ---> square对象

虽然容器里都是 graphic*,但它们真实指向的对象可以不同。


11. 父类指针能不能接住子类指针?

可以。

比如:

circle *p=new circle(4);
graphic *q=p;

这是合法的。

因为:

circle 是一种 graphic

所以:

circle* -> graphic*

可以自动转换。

你的代码里:

graphic *clone() const override
{
	return new circle(*this);
}

new circle(*this) 的类型是:

circle*

但是函数返回类型是:

graphic*

因为 circle* 可以转成 graphic*,所以合法。

可以理解成:

circle *tmp=new circle(*this);
graphic *ret=tmp;
return ret;

反过来不一定可以:

graphic *p=new circle(4);
circle *q=p;

这不能直接写。

因为 graphic* 不一定真的指向 circle,也可能指向 square
父类指针转子类指针需要额外判断,比如 dynamic_cast


12. clone() 为什么返回 graphic*

父类里写:

virtual graphic *clone() const=0;

它的意思是:

每个图形都要能复制自己,并返回一个父类指针

子类 circle 实现:

graphic *clone() const override
{
	return new circle(*this);
}

子类 square 实现:

graphic *clone() const override
{
	return new square(*this);
}

这就是多态复制。

如果当前对象是 circle,那就复制出一个新的 circle
如果当前对象是 square,那就复制出一个新的 square

外层统一用 graphic* 接住。

这样 graphic_list 不需要关心具体类型:

graphics.emplace_back(g.clone());

如果 g 实际是圆,就复制圆。
如果 g 实际是正方形,就复制正方形。


13. 为什么不能 return new graphic(*this);

square::clone() 里不能写:

return new graphic(*this);

原因有两个。

第一,graphic 是抽象类,不能创建对象:

new graphic(...)

不合法。

第二,语义也错。
square::clone() 里面,应该复制一个新的 square,而不是复制一个 graphic

正确写法是:

return new square(*this);

circle::clone() 里则是:

return new circle(*this);

总结:

circle::clone() -> new circle(*this)
square::clone() -> new square(*this)

14. new circle(*this) 是什么意思?

circle::clone() 里:

graphic *clone() const override
{
	return new circle(*this);
}

这里的:

*this

表示当前对象本体。

如果当前对象是:

circle c{4};

那么在 c.clone() 里面,*this 就是 c 自己。

所以:

new circle(*this)

等价于:

new circle(c)

它会调用 circle 的拷贝构造函数,复制出一个新的 circle 对象。

这和 move 不一样。

  • clone 是复制一份,原对象还保留
  • move 是资源转移,原对象进入一种合法但不应该继续依赖原内容的状态

clone() 后,原对象还是完整的。


15. my_list.add(circle{4}); 里的 circle{4} 真的创建对象了吗?

会。

my_list.add(circle{4});

这里 circle{4} 会创建一个临时 circle 对象。

它不是“完全不占内存”。
它是真实对象,只是生命周期很短,由编译器自动管理。

执行过程可以理解成:

创建临时 circle{4}
const graphic &g 绑定到这个 circle 临时对象
g.clone()
clone 里面 new circle(*this)
把新对象地址存进 vector
add 结束后,临时 circle{4} 销毁

所以这里其实有两个对象:

circle{4} 临时对象
new circle(*this) 复制出来的新对象

临时对象在 add 结束后销毁。
clone() 创建的新对象被保存进 graphics


16. for(auto *u:graphics) 为什么不是解引用两次?

代码:

for(auto *u:graphics)
{
	cout<<"perimeter:"<<(*u).get_perimeter()<<endl;
	cout<<"area:"<<(*u).get_area()<<endl;
}

这里不是解引用两次。

auto *u

这里的 * 是声明指针变量,不是解引用。

因为 graphics 的类型是:

vector<graphic *>

所以每个元素都是:

graphic*

因此:

for(auto *u:graphics)

等价于:

for(graphic *u:graphics)

真正解引用的是:

(*u).get_area()

它等价于:

u->get_area()

所以更常见写法是:

for(auto *u:graphics)
{
	cout<<"perimeter:"<<u->get_perimeter()<<endl;
	cout<<"area:"<<u->get_area()<<endl;
	cout<<endl;
}

17. 为什么注释里的循环会错?

错误写法:

for(auto u:graphics)
{
	cout<<"perimeter:"<<u.get_perimeter()<<endl;
	cout<<"area:"<<u.get_area()<<endl;
}

因为 graphics 是:

vector<graphic *>

所以 u 的类型是:

graphic*

u 是指针,不是对象。

指针不能用:

u.get_area()

应该用:

u->get_area()

或者:

(*u).get_area()

所以正确写法是:

for(auto u:graphics)
{
	cout<<"perimeter:"<<u->get_perimeter()<<endl;
	cout<<"area:"<<u->get_area()<<endl;
	cout<<endl;
}

这里 auto uauto *u 都可以。
区别是 auto *u 更明确表达 u 是一个指针。


18. 主函数为什么不能写 my_list.add(g:circle{r:4});

错误写法:

my_list.add(g:circle{r:4});

这不能编译。

原因有两个。

第一,C++ 普通函数调用不支持这种参数名写法:

g:...

C++ 不是 Python,不能这样写命名参数。

第二,circle{r:4} 也不适合这里。
你的 circle 有构造函数:

circle(double r)

所以应该通过构造函数传参:

circle{4}

正确写法是:

my_list.add(circle{4});

或者:

circle c{4};
my_list.add(c);

完整主函数:

int main()
{
	graphic_list my_list;
	my_list.add(circle{4});
	my_list.add(square{5});
	my_list.print_all();
	return 0;
}

19. 这段代码怎么体现面向对象思想?

这段代码体现了面向对象的四个核心思想。

封装

circle 把数据藏起来:

private:
	double r;
	const double pi{3.14};

外界不能直接乱改 r,只能通过函数使用:

get_area()
get_perimeter()

对象自己管理自己的数据。

抽象

graphic 只规定图形应该有什么能力:

get_area()
get_perimeter()
clone()

它不关心具体怎么算。

这就是抽象。

继承

class circle:public graphic
class square:public graphic

说明:

circle 是一种 graphic
square 是一种 graphic

子类继承父类接口,并提供自己的实现。

多态

graphic_list 里保存的是:

vector<graphic *> graphics;

表面上都是 graphic*,但真实对象可能是:

circle
square

调用时:

u->get_area()

如果 u 指向圆,就执行圆的面积公式。
如果 u 指向正方形,就执行正方形的面积公式。

这就是多态:

同一个接口,不同对象执行不同实现。


20. 这段代码的核心逻辑

这段代码最重要的逻辑是:

graphic 是抽象父类,定义统一接口
circle 和 square 是具体子类,实现接口
add 用 const graphic& 接收任意子类,避免对象切片
clone 用虚函数复制真实子类对象
vector<graphic*> 用父类指针统一保存不同子类对象
print_all 通过虚函数调用具体对象自己的实现

用一句话总结:

对象是带有数据和行为的具体实体;面向对象就是把问题拆成一批对象,让每个对象负责自己的数据和行为,再通过统一接口把它们组织起来。

在这段代码里,graphic_list 不需要知道圆和正方形具体怎么算面积。
它只需要知道:

每个 graphic 都可以 get_area()
每个 graphic 都可以 get_perimeter()

真正怎么算,由 circlesquare 自己决定。

这就是面向对象思想的价值。

posted @ 2026-06-15 18:37  dtcoke  阅读(12)  评论(0)    收藏  举报