c++ 如何写类(不带指针版)

发布于:2025-05-13 ⋅ 阅读:(12) ⋅ 点赞:(0)

c++中的类有两种类型,不带指针和带指针的。

1.不带指针的类:

这些类通常只包含简单的值成员(如 int、double、std::string、vector 等),不会直接管理资源或堆内存。

拷贝时是浅拷贝,直接复制成员变量的值

使用默认的拷贝构造、赋值运算符和析构函数没问题

2.带指针的类:

这些类直接或间接拥有指针成员,用于管理堆上的资源(如动态数组、文件句柄、网络连接等)。我们通常称这类为“拥有资源的类”或“RAII 类”。

拷贝时要进行深拷贝(复制出新的堆内存)

适合:类中用 new 创建了资源,管理文件、内存、锁等资源。

必须显式定义拷贝构造函数、赋值运算符和析构函数(即:三/五法则)

接下来我们以complex为例讲解一下不带指针的类如何定义。

头文件的写法:

#ifndef _COMPLEX_ //如果宏 _COMPLEX_ 没有被定义过,就进入下面的代码。
#define _COMPLEX_
​
#endif

上面的程序是 头文件防卫式声明(Include Guard),防止头文件被 重复包含,引起 重定义错误

主要是告诉编译器,如果没有定义过COMPLEX ,就编译下面的东西,而后的引入,就不需要处理这部分 逻辑。

这样,如果某个 .cpp 文件或其他头文件 多次包含同一个头文件,就只会真正处理一次这个头文件里的内容。

例如我们定义了一个complex.h

如果在某个 .cpp 文件中这样写:

#include "complex.h"
#include "complex.h"  // 又 include 一次

✅ 有 include guard,编译器只会处理一次 class Complex,不会报错。

❌ 如果没有 include guard,就会因为 Complex 被定义了两次而编译报错。

定义示例

现在我们要实现一个复数(将之定义为一个类),属性有:实部,虚部(double);现给出一个class 的声明,主要是为了让你认识class 声明的各个组成部分

#ifndef _COMPLEX_ //如果宏 _COMPLEX_ 没有被定义过,就进入下面的代码。
#define _COMPLEX_
class complex
{
public:
    complex(double r=0,double i=0):re(r),im(i){}
    //有些函数正在此直接定义,另一些在body之外定义
    complex& operator+=(const complex&);
    double real() const{return re;}
    double imag() const{return im;}
private:
    double re,im;
    frined complex&_doapl(complex*,const complex&)
}
​
​
#endif
1.inline 内联函数

函数若在class body内定义,便自动成为 inline候选人、像上面的real()和imag()这两个函数在class body中定义,就有资格成为inline,但最终是不是incline,我们不知道,由编译器决定。

具体我们再解释一下:

==》

1)类内定义的函数(如 real() 和 imag())自动具有 inline 的资格,即便你不写 inline 关键字。编译器根据函数的大小、调用频率、是否递归等 综合决定是否真正展开为内联。

内联不是强制行为,只是一个请求。

2)建议使用场景:

函数体非常小、频繁调用,如 getter/setter(正如上面代码中的 real() 和 imag())。

3)避免滥用 inline,特别是函数体复杂时,内联反而导致代码膨胀。

4)额外提醒:inline 函数的定义要放在 header 文件中,因为 inline 函数可能会被多个源文件调用,编译器需要在每个使用处都“看到”函数定义,所以必须在头文件中定义(即写在类中或头文件中)。

2.访问级别(access level)

在body中的代码可以用public,protected,private分成几大段;

数据应该放在private中,因为我们希望数据是封装起来的。不要被外界任意看到。

3.构造函数

在上面我们写的构造函数是:使用了默认参数,当你在创建对象时没有指明参数,就会使用默认参数进行属性的赋值。注意:其他函数也可以写默认参数(不是构造函数特有)

另外,构造函数有一种很特别多语法叫initialization list(初值列),这里的re(r),im(i)指的是把r设到属性re,把i设到属性im;这与直接在大括号中写是一样的,但这样写更加大气。因为实际上一个变量的值有两个阶段:初始化,赋值;在这里使用初值列就是初始化,而如果放到大括号中,则是赋值。

  complex(double r=0,double i=0):re(r),im(i){}

我们有几种调用构造函数的方式:

complex c1(2,1);
complex c2;
complex* p=new complex(4);

练习2

若我们想要再设计一个复数,只不过里面的实部,虚部是float,或是int;即区别只在于数据类型;我们可以使用模板;

template<typename T> //告诉编译器T这个类型未定
class complex
{
public:
    complex(T r=0,T i=0):re(r),im(i){}
    complex& operator+=(const complex&);
    T real() const{return re;}
    T imag() const{return im;}
private:
    T re,im;
}
​

使用时:

complex<double>c1(2.5,1.5);
complex<int>(2,6);

重载

1.同名的函数可以重载。(实际上在编译器看来,他们不同名)。

解释:在同一个作用域内,函数名相同,但参数个数或类型不同,这就是函数重载。

void print(int a);
void print(double b);
void print(int a, int b);

这些 print 函数在编译器眼中是不同的函数,因为它们的参数签名不同。

编译器通过参数类型和数量来区分它们,而不是仅仅依赖函数名。

2.构造函数可以有多个(重载)。

==》解释:

构造函数也支持重载,用于支持不同方式地初始化对象。

操作符重载

c++为了让+可以用来+诸如分数,对象等的功能。提供了操作符重载。

1.成员函数

我们的目的是实现一个支持复数运算的+=;固然要用到操作符重载,下面先给出怎么

使用这个+=;

complex c1(2,1);
complex c2(5);
//若左数有重载这个运算符+,则就可以使用
c2+=c1;

可以看到,我们写了c2+=c1; 调用了+=运算;编译器会去看左边的东西有没有实现这个+=;

接着我们来看到Complex类的成员函数,

众所周知,所有的成员函数一定带着一个隐藏的参数,this;所谓隐藏是指,你在定义函数时没有写这个参数,但是它在;this指向的是调用这个成员函数的对象;

故实际上下面的函数的参数是:(this,const complex& r);这便表示c2+=c1;调用的函数第一个参数就是this;(this 是一个指针,编译器会自动把c2的地址传进来)

inline complex& complex::operator+= (const complex& r)
{
    return _doap(this,r);
}
​
inline complex& _doap(complex* this,const complex& r){
    this->re+=r.re;
    this->im++r.im;
    return *this;
}

注:

1.所有的操作符只要有两个操作数,规则都如上述所言。

2.可能有人会将, _doap函数的取名怎么那么奇怪,实际上这是标准库里关于复数的代码;这是别人写的;

3.上面的返回值使用的是引用complex&,而不能是值传递的原因是否是因为有可能有: c1+=c2+=c3这样的事情发生 ?

==》是的。

1)使用返回值为引用 complex& 的最核心原因之一,就是为了支持像 c1 += c2 += c3; 这样的链式操作,而这是值传递(complex)做不到或效率很低的。

2)如果 operator+= 返回的是值(by value),会返回一个 临时 complex 对象,它不是 c2 本身,那么 c1 += (c2 += c3) 就变成了:c1 += 临时对象;这就意味着c1 没有用真正更新后的 c2。

3)而若返回引用c2 += c3 返回的是 c2 本身的引用;则c1 += (c2 += c3) 就等于 c1 += c2

2.非成员函数

既然是非成员函数,就没有this,而是全局函数

client的三种可能用法(如下)

complex c1(2,1);
complex c2;
c2=c1+c2;
c2=c1+5;//数学上复数可以和实部加在一起
c2=7+c1;

对于上面的用法,我们需要开发三个对应的函数。

inline complex opearator(const complex*x,const complex*y){
    return complex(real(x)+real(y),imag(x)+imag(y));
}
inline complex opearator(const complex& x,double y){
    return complex(real(x)+y,imag(x)+imag(y));
}
​
inline complex opearator(double x,const complex& y){
    return complex(x+real(y),imag(x)+imag(y));
}

备注:

1.我们知道返回值比返回引用的性能要差,但是为什么上面的三个函数返回值还是用value传递呢?==>

1)因为他们返回必定是一个local object;也及时说这些函数会创建了一个 临时对象(通常叫做 "local object"),这个对象只存在于 operator+ 函数体内部,如果你试图返回它的引用(如 return local_ref;),会出错!例如请看下面的错误示例:

inline const complex& operator+(const complex& x, const complex& y) {
    complex result(real(x) + real(y), imag(x) + imag(y));
    return result; // ❌ 错误:返回局部变量的引用!
}

2)而返回值(by value)是安全的,它返回的是一个匿名临时对象。C++ 会自动调用拷贝构造函数(或者通过返回值优化 RVO/NRVO 消除拷贝)

3)那为什么 += 可以返回引用?=》因为*this 是已经存在的对象,生命周期由调用者管理,返回它的引用是合法、安全、并且高效的。

2.上面的return complex(x+real(y),imag(x)+imag(y));用法是创建了临时对象,请你介绍一下?

=》临时对象的特点:没有变量名,如:complex(1, 2);是“右值”(rvalue)的一种典型表现。如果作为函数的返回值,会被复制或优化转移给接收方;

练习

下面的两个函数是标准库中对于复数的正号和负号的重载,我们很奇怪对于operator +并没有像我们上面分析的那样要返回一个局部对象(因为对于传进来的x没有做任何改变),理论上可以返回引用,但它为什么还是要返回值呢?

==》它这样写没有错,但是不够高效和大气。这里能够让我们知道:标准库并不是圣经。

inline complex operator + (const complex& x){   
    return x;
}
inline complex operator - (const complex& x){   
    return complex(-real(x),-imag(x));
}
练习2

当我们的编译器看到下面的用法时,会将<<作用在左边身上;而cout<<conj(c1);左边是cout,cout是标准库定义的,但是因为cout里面的东西可能早就写好了,它不可能认识针对负数的<<修改它的类定义;所以对于这样的运算符重载我们不能写成成员函数,而只能定义为非成员函数。

即ostream& operator<<(ostream& os, const complex& x);

complex c1(2,1);
cout<<conj(c1);

我们现在就来看定义的函数:

#include<iostream.h>
ostream& operator(ostream& os,const complex& x){
    return os<<'('<<real(x)<<','<<imag(x)<<')';
}

1)可以看到上面的第一个参数ostream& os并没有加const,那是因为每一次的操作都会去改变os的状态。(这是标准库的人定义的规则,我们查手册才知晓)而加了const就表示不可以去改动这个os。

2)对于返回值,为了满足使用者连续输出的习惯,这里返回的是引用,ostream&

总结

1要会用构造函数的初始列

函数要不要加const,该加就要加

函数的传递尽量考虑引用传递,如果有副作用,再考虑值传递;且要看要不要加const

返回值是返回引用还是值

数据放在priivate,函数放在public