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