c++类和对象

封装的含义

封装: 就是将抽象出来的对象的属性和行为结合成个独立的单位,并尽可能隐藏对象的内部细节。

封装有两个含义:

(1)是把对象的全部属性和行为结合在一起,形成一个不可分割的独立单位

(2)是尽可能隐蔽藏对象的内部细节,对外形成一道屏障通过公有行为充当外部接口:对象的私有属性只能由对象的公有行为来读取和修改。

类的含义

类是具有相同属性和方法的一类对象集合的抽象,它包含数据抽象(即数据成员)和行为抽象(即成员函数)

定义类的过程就是对问题进行抽象和封装的过程

类的定义

上方定义中的public,protected,private,即为访问修饰符。

需要注意的是:在结构体struct中,不进行声明的话,内部成员默认为公有的,及程序内任意函数都可调用其内部成员。

而在类class中,如果不进行声明,则默认为私有成员,即仅允许本类成员函数或友元中函数进行访问。

# include <iostream>
using namespace std;

class point
{
private:
	int x, y;
	double c;
public:
	void set1(int a, int b)
	{
		x = a;
		y = b;
	}
	void show(void)
	{
		cout << x;
		cout << y;
	}
};

int main()
{
	point a;
	// a.x = 1; error
	// a.y = 1;
	// a的x成员是private属性的,不在外部引用

	a.set1(1, 1); //set1是public方法,所以作为成员函数被使用
	a.show();
}

成员函数

类的成员函数是实现类的行为属性的成员。一般将成员函数在类内声明为函数原型,在类外具体实现成员函数,使条理清晰。

成员函数在类外定义的形式:

# include <iostream>
using namespace std;

class point
{
private:
	int x, y;
	double c;
public:
	void set1(int a, int b)
	{
		x = a;
		y = b;
	}
	void show(void); //要函数声明
};

void point::show()
{
	cout << x << y;
}

int main()
{
	point a;
	// a.x = 1; error
	// a.y = 1;
	// a的x成员是private属性的,不在外部引用

	a.set1(1, 1); //set1是public方法,所以作为成员函数被使用
	a.show();
}

c++中类成员函数的相互调用

一、同一个对象的类成员函数相互调用

对象

概念:对象是类的实例或实体。类与对象的关系,就像是数据类型(int,char,bool)和该类型的变量(变量名,例a,b,c等)之间的关系。(相当于你写了一个数据类型(类),如果你想用它,就必须通过实实在在的变量名(类的对象)来构造这样的一个数据) 。

从类外访问类的成员(和结构体类似)

圆点型

指针型

# include <iostream>
using namespace std;

class point
{
private:
	int x, y;
	double c;
public:
	void set1(int a, int b)
	{
		x = a;
		y = b;
	}
};
int main()
{
	point a;
	// a.x = 1; error
	// a.y = 1;
	// a的x成员是private属性的,不在外部引用

	a.set1(1, 1); //set1是public方法,所以作为成员函数被使用
	point* point1;
	point1 = &a; 
	point1->set1(2, 2);

	point p1[3];
	for (int i = 0; i < 3; ++i)
	{
		p1[i].set1(i, i);
	}

}

构造函数

如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

定义:

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。

其特征如下:

1. 函数名与类名相同。

2. 无返回值。

3. 对象实例化时编译器自动调用对应的构造函数。

4. 构造函数可以重载。

实例:

 class Date
 {
  public:
  
        //构造函数——与类同名,且没有返回值
      Date(int year, int month, int day)
     {
          _year = year;
          _month = month;
          _day = day;
     }
 
  void Print() {
           cout << _year<<"-"<< _month << "-" << _day << endl;
      }
 
  private:
      int _year;
      int _month;
      int _day;
 };
  
 
  int main(){
 
    Date d1(2015, 1, 1); 
    Date d2(2022,9,1);
    d1.Print();
    d2.Print();
 }

当Date类创建d1,d2对象时,会自动调用构造函数,实参2015传给形参year,1传给month,1传给day......完成d1,d2对象的成员函数初始化赋值。 

class Date
{
public:
 
    //构造函数
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
 
    //无参构造函数
    Date() {
        _year = 1999;
        _month = 10;
        _day = 13;
    }
 
 
    void Print() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
 
private:
    int _year;
    int _month;
    int _day;
};
 
int main() {
    Date d1(2015, 1, 1);
    Date d2(2022, 9, 1);
    d1.Print();
    d2.Print();
    Date d3;
    Date d4;
    d3.Print();
    d4.Print();
    return 0;
}

注:创建d3,d4对象时,不带参数所以自动调用无参构造函数Date() (即函数重载)

二义性调用

class Date
{
public:
    //全缺省构造函数
    Date(int year=1, int month=1, int day=1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
 
    //无参构造函数
    Date() {
        _year = 1999;
        _month = 10;
        _day = 13;
    }
 
 
    void Print() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
 
private:
    int _year;
    int _month;
    int _day;
};
 
int main() {
  
    Date d5;
    Date d6;
    d5.Print();
    d6.Print();
    return 0;
}

构造函数不仅能够完成对象成员变量的初始化,还可以做判断功能:


class Date {
public:
	Date(int year = 1199, int month = 12, int day = 15) {
 
        //可以对对象的实参做判断功能
		if(!(year >= 1 && (month >= 1 && month <= 12) && (day >= 1 && day <=             
         GetMonthDay(year, month)))) {
 
			cout << "该日期为非法日期" << endl;
		}
		else {
			cout << "该日期为合法日期" << endl;
		}
		_year = year;
		_month = month;
		_day = day;
		cout << _year << "-" << _month << "-" << _day << endl;
	}
 
    //获得月份天数
	int GetMonthDay(int year, int month) {
		int MonthDay[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if ((month == 2) && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
			return 29;
		}
		else {
			return MonthDay[month];
		}
	}
private:
	int _year;
	int _month;
	int _day;
};
 
int main() {
	Date d1(2022, 2, 40);
	Date d2(-1999, 15, 30);
	Date d3(2022, 10, 15);
}

成员初始化列表

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。采用从初始化列表的缘故是因为成员变量中会存在一些特殊情况,只能由初始化列表去赋值。

class Date
{
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日

public:
    //构造函数
	Date(int year, int month, int day)
        //初始化列表
		:_year(1999)
		,_month(10)
		,_day(30)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    
	void Print()
	{
		cout << "Print()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
 
};
 
int main(){
    Date d1;
}

当创建对象d1时,调用该类构造函数,在进入构造函数前,各成员变量都是随机值,编译器会先进入初始化列表,为各成员变量初始化值。

也可以将构造函数和初始化列表混合着用:

typedef int DataType;
class Stack
{
public:
	Stack(size_t capa)
		:_size(0)
		, _capacity(capa)
	{
		//构造函数内部
		_array =(DataType*)malloc(capa * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
	}

初始化列表最重要的作用就是为特殊的成员变量提供初始化帮助: 

 当我们在定义const变量时,往往需要定义时就要给初始化值:

数并不能满足需求,需要用到初始化列表。

情况1: 成员变量中有const成员—— 但列表处成员不被初始化时


class B{
public:
	B()
		:_n(10)	//初始化列表
	{}
private:
	const int _n; // const 
	int _m;
};

总结:每个成员都要走初始化列表,就是成员没有在初始化列表写,也会走。但初始化列表只有_n被初始化,而_m没有,所以赋给随机值 

情况1: 成员变量中有const成员—— 给缺省值时:


class B
{
public:
	B()
		:_n(10)	//初始化列表
	{}
private:
	const int _n; // const 
	int _m=1;	//缺省值
};
int main() {
	B b2;
}

总结:因为成员变量处给了缺省值,但此时_m还处于随机值,直到运行时编译器进入构造函数初始化列表,_m才会使用缺省值 

 情况1: 成员变量中有const成员—— 列表处成员不仅初始化,还有缺省值


class B
{
public:
	B(int a, int ref)
		:_n(10)	//初始化列表
		, _m(3)
	{}
private:
	const int _n; // const 
	int _m=100;	//缺省值
};

总结:这次,_m不仅有缺省值,还在初始化列表中被初始化,而编译器会优先选择初始化列表的值初始化列表使用权>缺省值使用权限,所以最终_m最终是按照初始化列表的赋值使用,其值为3。

情况2:成员是自定义类型成员时:

class A{
public:
	//构造函数,并不是默认构造
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
 
class B{
public:
	B()
		:_n(10)	//初始化列表
	{}
private:
	const int _n; // const 
	A _aa;	
};
 
int main() {
	B b4;
}

总结:编译器执行对象b4的创建时,会进入类B的构造函数,因为成员变量有自定义类型 _aa,所以编译器会进入类A中找它的默认构造,但类A中没有默认构造,类A中只有自己写的构造函数,此时也就无法给自定义类型成员_aa赋值,所以编译器会报错。

优化方案1:使得类A有默认构造函数(这个默认构造可以是全缺省的构造函数,也可以是无参构造,或者直接不写由编译器自动生成的构造函数)

优化方案2:让类A放弃默认构造,采用初始化列表

调用b的成员时,即调用A _aa时会到A类中调用它的默认构造,没有默认构造时,使用初始化列表,也不会报错。 

默认构造函数

如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成。

默认构造函数有三种形式:

重点介绍一下编译器自己生成的:


class Date
{
 public:
     
     //没有写构造函数
 
     void Print()
     {
     cout << _year << "-" << _month << "-" << _day << endl;
     }
 
 private:
     int _year;
     int _month;
     int _day;
};
 
int main()
{
    Date d1;
    return 0;
}

当创建d1对象时,自动调用构造函数,因为我们没有亲自去写构造函数,所以编译器自动生成一个默认的无参构造函数,这种构造函数既不会让其成员变量置成零,也不会做其他任何事情,它生成的构造函数只为确保程序能够正常运行,赋值的事情不归它管,该类中的成员变量值经过构造后会变成随机值。

有的小伙伴会问了,系统生成的默认构造函数没什么用,毕竟这些成员变量并不能被赋我们想要的具体的值,最终还是得要我们自己去写它,那么创造出这个机制的意义在哪里?

 其实C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,而自定义类型就是我们使用class/struct/union等自己定义的类型。对于内置类型的成员变量,编译器不会管,所以成员变量成为随机值(让其自生自灭),对于自定义类型的成员变量,编译器会调用其自定义类型的构造函数,也就是说编译器只处理自定义类型的成员变量。

析构函数

定义:

析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

特性:

 析构函数是特殊的成员函数,其特征如下:

# include <iostream>
using namespace std;
class Stack
{
private:
    int* _array;
    int _capacity;
    int _size;
public:
    //构造函数
    Stack(int  capa = 4)
    {
        _array = (int*)malloc(sizeof(int) * capa);
        if (NULL == _array)
        {
            perror("malloc申请空间失败!!!");
            return;
        }
        _capacity = capa;
        _size = 0;
    }
    void Push(int data) {
        // 扩容
        _array[_size] = data;
        _size++;
    }

    //析构函数
    ~Stack() {
        if (_array) {
            free(_array);
        }
        _array = NULL;
        _capacity = 0;
        _size = 0;
    }

int main() {
    Stack st;    //创建对象后自动调用构造函数——完成初始化操作
    st.Push(1);
    st.Push(2);
    st.Push(3);
    return 0;
}

析构函数的调用是在执行return 0后,函数即将销毁时自动调用它,然后堆区空间就会被释放,且其他变量清零。 

默认析构函数

默认析构函数与默认构造函数同理,都是我们不生成时,编译器自动生成一个析构函数,但编译器生成的也只能处理一些简单类型的成员变量,例如日期类:


class Date {
public:
    Date(int year, int month, int day)
    {
    _year = year;
    _month = month;
    _day = day;
    }
 
    void Print() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
    //没有写析构函数
 
private:
    int _year;
    int _month;
    int _day;
};
 
int main() {
    Date d1(2022, 10, 5);
    Date d2(2021, 9, 1);
    return 0;
}

  对于复杂类型的成员变量,编译器自动生成的析构函数并不能完成最后的清理工作,比如:对于堆区空间的生成,使用fopen函数打开的文件,最后需要手动释放(fclose/free),可是默认析构函数并不能做到这么智能,所以还是得自己去写析构函数!!!

拷贝构造函数

如果已经存在一个对象,我想对这个对象再复制一份,该怎么做呢?

使用拷贝构造函数

拷贝构造函数是类的六大特殊成员函数之一,它是构造函数的一个重载形式,且参数只有一个,必须使用引用传参

而且由于拷贝并不需要改变参数,所以参数部分还要用 “const” 来修饰

下面是用例示范:

class Date {
public:
	Date(int year = 2022, int month = 8, int day = 20)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year = 2022, int month = 8, int day = 20)" << endl;
	}

	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(const Date& d)" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main() {
	Date d1;
	Date d2(d1);
	Date d3 = d1;
	return 0;
}

运行结果如下:

拷贝构造是已经存在的对象拷贝给即将要创建的对象,所以 d3 虽然像是去调用赋值函数重载函数,实际上还是拷贝构造。


为什么拷贝构造要传引用

首先传值调用的话,我们要明确一点,就是形参是实参的临时拷贝

以下面的代码为例:

现在已经明确了,传参时会调用一次拷贝构造函数。

那么如果拷贝构造的参数部分也是传值调用呢?

每次调用拷贝构造函数传参时都要进行临时拷贝,临时拷贝又要调用拷贝构造函数,如此往复,就引发了无穷递归。

默认拷贝构造函数

class Date {
public:
	Date(int year = 2022, int month = 8, int day = 20) 
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year = 2022, int month = 8, int day = 20)" << endl;
	}

	void Print() {
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main() {
	Date d1;
	Date d2(d1);
	d2.Print();
	return 0;
}

运行结果如下:

d2 也成功完成拷贝构造了。

拷贝构造函数毕竟是六个特殊的成员函数之一,所以我们不写编译器也是会自动生成的,编译器自动生成的这个就是所谓的默认拷贝构造函数

同样地,和编译器默认生成的构造函数一样,默认拷贝构造函数对内置类型会完成拷贝,对自定义类型会去调用它的拷贝构造函数:

class A {
public:
	A() 
		:_a(0)
	{
		cout << "A()" << endl;
	}

	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}

private:
	int _a;
};

class B {
public:
	B()
		:_b(0)
	{
		cout << "B()" << endl;
	}

private:
	int _b;
	A aa;
};

int main() {
	B b1;
	B b2(b1);
	return 0;
}

运行结果如下:

前面调用的编译器自动生成的拷贝构造函数,也完成了我们想要的效果。

既然编译器自动生成的默认拷贝构造函数也能完成拷贝构造,那我们是不是可以不写了呢?

看下面一段代码:

class Stack {
public:
	Stack(int k = 4) 
		:_arr(new int[k])
		,_size(0)
		,_capacity(k)
	{
		cout << "Stack(int k = 4)" << endl;
	}

	~Stack() {
		cout << "~Stack()" << endl;
		delete[] _arr;
		_arr = nullptr;
		_size = _capacity = 0;
	}

private:
	int* _arr;
	int _size;
	int _capacity;
};

int main() {
	Stack st1;
	Stack st2(st1);
	return 0;
}

这是粗略地写了一个 Stack 类,~Stack() 是析构函数,出对象的作用域会自动调用,完成对象内容的清理,这里是释放掉 _arr 指向的空间并置空。

调试看一下监视窗口:

st1 和 st2 的 _arr 竟然都指向一块空间。

这里就可以看出系统默认生成的拷贝构造函数完成的是

因为 arr 只是一个指针变量,它的值是指向的空间的首地址,所以 st2.arr 是直接得到了 st1.arr 的值,所以他俩指向一块空间,实际上就是他俩之间进行了值拷贝。

很显然,这种场景下编译器默认生成的拷贝构造函数已然满足不了我们的需求,这时就要我们自己写一个拷贝构造,自己完成深拷贝。

那什么是深拷贝呢?

浅拷贝和深拷贝

前面我们已经知道,编译器默认生成的拷贝构造函数只能进行简单的浅拷贝

所谓浅拷贝,其实就是按内存存储,按字节序完成拷贝。

形象一点就是,假设一个四个字节的变量存放的内容是 0x11223344,,那么浅拷贝就会把它存放的内容依次复制过去,拷贝后的结果也是 0x11223344。如果这块空间存放的是地址,那么拷贝到的也是同样的地址,这就会导致有两个指针指向同一块地址,这并不符合我们想要的拷贝效果。

我们想要的拷贝效果是拷贝构造的对象指向一块与源对象不同的空间,但空间大小和存放的内容都相同。

这其实就是所谓的深拷贝

知道了想要的功能,下面就去实现它。

还是以我们自己写的 Stack 类为主
 

class Stack {
public:
	Stack(int k = 4) 
		:_arr(new int[k])
		,_size(0)
		,_capacity(k)
	{}

	Stack(const Stack& st)
		:_arr(new int[st.capacity()])
		,_capacity(st.capacity())
		,_size(st.size())
	{
        memcpy(_arr, st._arr, st.size() * sizeof(int));
    }

	~Stack() {
		delete[] _arr;
		_arr = nullptr;
		_size = _capacity = 0;
	}

	int size() const {
		return this->_size;
	}

	int capacity() const {
		return this->_capacity;
	}

private:
	int* _arr;
	int _size;
	int _capacity;
};

int main() {
	Stack st1;
	Stack st2(st1);
	return 0;
}

可见 st1._arr 和 st2.arr 确确实实指向了两块空间,这里并没有存放数据,所以没看出来数据的拷贝,不过无伤大雅~

总结一下,上面我们简单认识了一下浅拷贝和深拷贝。编译器自动生成的默认拷贝构造函数只能帮我们完成浅拷贝,所以当没有深拷贝需求时是可以不用我们自己写拷贝构造函数的。但当成员变量有指针且指向一块空间,需要拷贝构造的对象需要指向另外一块空间时,就需要我们自己写拷贝构造完成深拷贝了。

this指针

1.this指针的引出


class Date
{
public:
	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1, d2;
	d1.SetDate(2022, 5, 11);
	d2.SetDate(2022, 5, 12);
	d1.Display();
	d2.Display();
	return 0;
}

输出结果: 

我们首先可以通过汇编来看看,d1,d2调用的函数是否相同。

我们可以发现,最终打印的时候调用的Display()是同一个函数, 那么既然d1,d2调用的都是同一个函数,编译器如何知道d1是2022-5-11,d2是2022-5-12呢?Display()都访问的_year,_month,_day。而且去公共代码区访问的Display(),这是为什么呢?

这是因为C++在这段代码中做出手脚,C++在这里增加了一个this指针,这里是因为Display会增加一个this形参。

C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

在调用的时候也传的是各自的地址。这样就十分清晰明了了。这就是隐含的this指针

注意:我们不能显示的写出来,因为他是隐含的,我们不能抢了编译器的活。但是我们可以直接在类里面用。

2.this指针的特性

在真正的编译器中this指针的用const修饰的,this指针本身是不能被修改的,但是内容是可以修改的。并且我们是可以使用的

我们可以在类中打印一下this指针,并且我们在也同时打印一下d1和d2的地址,我们来看一下:


class Date
{
public:
	void Display()
	{
        //使用this指针
		cout << this << endl;
		cout << _year << "-" << _month << "-" << _day << endl;
	}	
 
	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1, d2;
	cout <<"d1:"<< & d1 << endl;
	cout <<"d2:"<< & d2 << endl;
 
	d1.SetDate(2022, 5, 11);
	d2.SetDate(2022, 5, 12);
 
	d1.Display();
	d2.Display();
	return 0;

运行结果: 

并且我们还能这样写,但是我们不能显示的写出Date* this。

我们接下来再看看this指针是不能修改的,大家看下面这个能过吗?答案肯定是不能的,因为this是被const修饰的,不能修改this指针的

  • this指针还可以充当返回值,返回的是类的本身,*this就是类本身(这里就可以说明,this指针的就是一个指向类的指针,只不过它不需要定义,出了类就不存在,所以要用引用类型的函数返回类型)

class GG
{
public:
	GG(string name, int age) :name(name), age(age) {}
	void print()
	{
		cout << this->name << " " << this->age << endl;
	}
	GG& returnGG()
	{
		return *this;
	}
protected:
	string name;
	int age;
};
int main()
{
	GG gg("哥哥吴彦祖", 38);
	gg.returnGG().returnGG().returnGG().returnGG().returnGG().returnGG().print();
	/*打印结果
	哥哥吴彦祖 38
	*/
	return 0;
}

类的静态变量

一、C++中静态成员

在类定义中,它的成员(包括成员变量和成员函数),这些成员可以用关键字 static 声明为静态的,称为静态成员。 不管这个类创建了多少个对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象共享。

静态成员变量

静态成员变量是类的成员变量,它们不属于任何对象,也不包含在类的对象分配空间中。静态成员变量只有一份,可以在多个对象之间共享,通常用于表示全局性质的数据,比如计数器、标识符等。

静态成员变量的定义和普通成员变量的定义类似,但需要在变量名前面加上static关键字。在静态成员变量的定义中,不需要加上static关键字,但必须要有类型和名字。

以下是一个静态成员变量的定义示例:


class MyClass {
public:
    static int m_staticVar;
};
 
int MyClass::m_staticVar = 0;

在上述代码中,我们定义了一个名为MyClass的类,它包含一个公有的静态成员变量m_staticVar。在类外部,我们需要对其进行定义和初始化。

静态成员变量可以通过类名或者对象名来引用。对于通过类名引用的静态成员变量,需要加上作用域运算符,比如MyClass::m_staticVar。对于通过对象名引用的静态成员变量,也需要加上作用域运算符,比如myObject.m_staticVar。


int main() {
  // 通过类名来访问静态成员变量
  MyClass::m_staticVar = 10;
 
  // 创建对象来访问静态成员变量
  MyClass myObject;
  myObject.m_staticVar = 20;
 
  // 输出静态成员变量的值
  cout << MyClass::m_staticVar << endl; // 20
  cout << myObject.m_staticVar << endl; // 20
 
  return 0;
}

静态常量

  静态常量是指在程序运行期间不会改变的(值不能改变),可以在全局范围内使用的常量。在C++中,我们可以使用静态成员变量来定义静态常量。静态常量与类的实例化无关,只有一份,可以在多个对象之间共享,比如定义数学常数、文件路径等。

由于静态常量和静态变量很相似(只是值是固定的),为了区分于静态变量,最好在类内部就进行初始化。

以下是一个使用静态成员变量定义静态常量的示例代码:

class MyClass {
public:
    static const float PI= 3.1415926;
};

在上述代码中,我们定义了一个名为MyClass的类,它包含一个公有的静态成员常量PI。在类内部,我们将其初始化为3.1415926。

        静态常量可以通过类名或者对象名来引用。对于通过类名引用的静态常量,需要加上作用域运算符,比如MyClass::PI。对于通过对象名引用的静态常量,也需要加上作用域运算符,比如myObject.PI。(由于使用和静态变量无区别,这里就不作演示)

静态成员函数

 静态成员函数是类的成员函数,它们不属于任何对象,可以直接通过类名调用。静态成员函数通常用于执行全局性质的操作,比如工厂函数、单例模式等。

在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数属于整个类,是该类所有对象共享的成员函数,而不属于类中的某个对象。静态成员函数的作用不是为了对象之间的沟通,而是为了处理静态数据成员。定义静态成员函数的格式如下:

static 返回类型 静态成员函数名(参数表);

与静态数据成员类似,调用公有静态成员函数的一般格式有如下几种:

类名::静态成员函数名(实参表);
对象.静态成员函数名(实参表);
对象指针->静态成员函数名(实参表);

静态成员函数的定义和普通成员函数的定义类似,但需要在函数名前面加上static关键字。静态成员函数只能访问静态变量,不能访问普通成员变量 静态成员函数的使用和静态成员变量一样 静态成员函数也有访问权限,普通成员函数可访问静态成员变量、也可以访问非静态成员变量


#include <iostream>
using namespace std;
 
class MyClass {
public:
  // 普通成员函数可以访问 static 和 non-static 成员属性
  void changeParam1(int param) {
    mParam = param;
    sNum = param;
  }
  // 静态成员变量和函数只能访问 static 成员属性
  static int sNum;
  static void changeParam2(int param) {
    sNum = param;
  }
private:
  int mParam;
};
 
// 定义静态成员变量
int MyClass::sNum = 0;
 
int main() {
  // 通过类名来访问静态成员变量和函数
  MyClass::sNum = 10; // 修改静态成员变量
  MyClass::changeParam2(20); // 调用静态成员函数
  cout << MyClass::sNum << endl; // 20
 
  // 创建对象来访问非静态成员
  MyClass obj;
  obj.changeParam1(30); // 修改非静态成员变量
  cout << obj.sNum << endl; // 20
  return 0;
}

在上述代码中,我们定义了一个名为MyClass的类,它包含一个非静态成员变量mParam和两个静态成员函数changeParam2和changeParam3。

        在公有区域中,我们定义了一个静态成员变量sNum,并在类外部对其进行初始化。我们还实现了一个通过对象调用的普通成员函数changeParam1,在函数中修改了非静态和静态成员变量的值。

        在main函数中,我们通过类名来访问静态成员变量和静态成员函数,并演示了如何使用对象来访问非静态成员变量和非静态成员函数。

#include <iostream>
using namespace std;

class Score{
private:
	int mid_exam;
	int fin_exam;
	static int count;     //静态数据成员,用于统计学生人数
	static float sum;     //静态数据成员,用于统计期末累加成绩
	static float ave;     //静态数据成员,用于统计期末平均成绩
public:
	Score(int m, int f);
	~Score();
	static void show_count_sum_ave();   //静态成员函数
};

Score::Score(int m, int f)
{
	mid_exam = m;
	fin_exam = f;
	++count;
	sum += fin_exam;
	ave = sum / count;
}

Score::~Score()
{

}

/*** 静态成员初始化 ***/
int Score::count = 0;
float Score::sum = 0.0;
float Score::ave = 0.0;

void Score::show_count_sum_ave()
{
	cout << "学生人数: " << count << endl;
	cout << "期末累加成绩: " << sum << endl;
	cout << "期末平均成绩: " << ave << endl;
}

int main()
{
	Score sco[3] = {Score(90, 89), Score(78, 99), Score(89, 88)};
	sco[2].show_count_sum_ave();
	Score::show_count_sum_ave();

	return 0;
}

类的友元

类的主要特点之一是数据隐藏和封装,即类的私有成员(或保护成员)只能在类定义的范围内使用,也就是说私有成员只能通过它的成员函数来访问。但是,有时为了访问类的私有成员而需要在程序中多次调用成员函数,这样会因为频繁调用带来较大的时间和空间开销,从而降低程序的运行效率。为此,C++提供了友元来对私有或保护成员进行访问。友元包括友元函数和友元类。

友元类声明

代码示例 :

class Student
{
private:
	// 声明 StudentCaculate 类是 Student 类的友元类
	// 在 StudentCaculate 类中可以访问 Student 类中的私有成员
	friend class StudentCaculate;
}

友元类单向性

友元类作用

友元函数

告诉编译器全局函数是 B类 的好朋友,可以访问B对象的私有成员

  1. 因为友元函数不是类的成员,所以它不能直接访问对象的数据成员,也不能通过this指针访问对象的数据成员,它必须通过作为入口参数传递进来的对象名(或对象指针、对象引用)来访问该对象的数据成员。
  2. 友元函数提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。尤其当一个函数需要访问多个类时,友元函数非常有用,普通的成员函数只能访问其所属的类,但是多个类的友元函数能够访问相关的所有类的数据。

例子:一个函数同时定义为两个类的友元函数

#include <iostream>
#include <string>
using namespace std;

class Score;    //对Score类的提前引用说明
class Student{
private:
	string name;
	int number;
public:
	Student(string na, int nu) {
		name = na;
		number = nu;
	}
	friend void show(Score &sc, Student &st);
};

class Score{
private:
	int mid_exam;
	int fin_exam;
public:
	Score(int m, int f) {
		mid_exam = m;
		fin_exam = f;
	}
	friend void show(Score &sc, Student &st);
};

void show(Score &sc, Student &st) {
	cout << "姓名:" << st.name << "  学号:" << st.number << endl;
	cout << "期中成绩:" << sc.mid_exam << "  期末成绩:" << sc.fin_exam << endl;
}

int main() {
	Score sc(89, 99);
	Student st("白", 12467);
	show(sc, st);

	return 0;
}

class Building
{
	// 告诉编译器 laoWang全局函数是 Building类  的好朋友,可以访问Building对象的私有成员
	friend void laoWang1(Building *building);
	friend void laoWang2(Building &building);
	friend void laoWang3(Building building);

public:

	Building()
	{
		m_SittingRoom = "客厅";
		m_BedRoom = "卧室";
	}
	
	string m_SittingRoom;	// 客厅

private:

	string m_BedRoom;		// 卧室
}

下面给出全局函数做友元访问类的私有成员的完整示例代码

#include <iostream>
#include <string>

using namespace std;

// 房屋类
class Building
{
	// 告诉编译器 laoWang全局函数是 Building类  的好朋友,可以访问Building对象的私有成员
	friend void laoWang1(Building *building);
	friend void laoWang2(Building &building);
	friend void laoWang3(Building building);

public:

	Building()
	{
		m_SittingRoom = "客厅";
		m_BedRoom = "卧室";
	}
	
	string m_SittingRoom;	// 客厅

private:

	string m_BedRoom;		// 卧室
};



//全局函数
void laoWang1(Building *building)
{
	cout << "隔壁老王 全局函数 正在访问:(地址传递) " << building->m_SittingRoom << endl;

	cout << "隔壁老王 全局函数 正在访问:(地址传递) " << building->m_BedRoom << endl;
}

void laoWang2(Building &building)
{
	cout << "隔壁老王 全局函数 正在访问:(引用传递) " << building.m_SittingRoom << endl;

	cout << "隔壁老王 全局函数 正在访问:(引用传递) " << building.m_BedRoom << endl;
}

void laoWang3(Building building)
{
	cout << "隔壁老王 全局函数 正在访问:( 值传递 ) " << building.m_SittingRoom << endl;

	cout << "隔壁老王 全局函数 正在访问:( 值传递 ) " << building.m_BedRoom << endl;
}

void test()
{
	Building building;
	laoWang1(&building);
	laoWang2(building);
	laoWang3(building);
}


int main()
{
	test();
}
#include "iostream"
using namespace std;

class Student
{
public:
	// 带参构造函数
	Student(int age = 1, int height = 1)
	{
		this->age = age;
		this->height = height;
		cout << "执行 Student 的构造函数" << endl;
	}

	~Student()
	{
		cout << "执行 Student 的析构函数" << endl;
	}

public:
	// 打印类数据
	void print()
	{
		cout << " age = " << age  << " , height = " << height << endl;
	}

private:
	// 声明 StudentCaculate 类是 Student 类的友元类
	// 在 StudentCaculate 类中可以访问 Student 类中的私有成员
	friend class StudentCaculate;

	// 声明友元函数 
	friend void changeAge(Student* s, int age);

private:
	int age;		// 年龄
	int height;		// 身高
};

class StudentCaculate
{
public:
	void fun()
	{
		cout << "age + height = " << student.age + student.height << endl;
	};
public:
	// 此处会自动调用默认的构造函数
	// 默认值都为 1
	Student student;
};

// 在友元函数中 访问 age 私有属性
void changeAge(Student* s, int age)
{
	s->age = age;
}



int main() {
	
	// 声明 Student 友元类 StudentCaculate 对象
	StudentCaculate sc;

	// 调用 sc 对象中的 fun , 其中调用了 Student 的私有成员
	sc.fun();


	// 为 StudentCaculate 设置一个非默认值
	sc.student = Student(10, 120);

	// 调用 sc 对象中的 fun , 其中调用了 Student 的私有成员
	sc.fun();
	

    // 控制台暂停 , 按任意键继续向后执行
    system("pause");

    return 0;
}

将成员函数声明为友元函数

一个类的成员函数可以作为另一个类的友元,它是友元函数中的一种,称为友元成员函数。友元成员函数不仅可以访问自己所在类对象中的私有成员和公有成员,还可以访问friend声明语句所在类对象中的所有成员,这样能使两个类相互合作、协调工作,完成某一任务。

#include <iostream>
#include <string>
using namespace std;

class Score;    //对Score类的提前引用说明
class Student{
private:
	string name;
	int number;
public:
	Student(string na, int nu) {
		name = na;
		number = nu;
	}
	void show(Score &sc);
};

class Score{
private:
	int mid_exam;
	int fin_exam;
public:
	Score(int m, int f) {
		mid_exam = m;
		fin_exam = f;
	}
	friend void Student::show(Score &sc);
};

void Student::show(Score &sc) {
	cout << "姓名:" << name << "  学号:" << number << endl;
	cout << "期中成绩:" << sc.mid_exam << "  期末成绩:" << sc.fin_exam << endl;
}

int main() {
	Score sc(89, 99);
	Student st("白", 12467);
	st.show(sc);

	return 0;
}

说明:

  1. 一个类的成员函数作为另一个类的友元函数时,必须先定义这个类。并且在声明友元函数时,需要加上成员函数所在类的类名;

友元类

对象成员

对象数组与对象指针

对象数组

类名 数组名[下标表达式]
用只有一个参数的构造函数给对象数组赋值
Exam ob[4] = {89, 97, 79, 88};
用不带参数和带一个参数的构造函数给对象数组赋值
Exam ob[4] = {89, 90};
用带有多个参数的构造函数给对象数组赋值
Score rec[3] = {Score(33, 99), Score(87, 78), Score(99, 100)};

对象指针

每一个对象在初始化后都会在内存中占有一定的空间。因此,既可以通过对象名访问对象,也可以通过对象地址来访问对象。对象指针就是用于存放对象地址的变量。声明对象指针的一半语法形式为:类名 *对象指针名

Score score;
Score *p;
p = &score;
p->成员函数();

用对象指针访问对象数组

# include <iostream>
using namespace std;

class score
{
public:
	int a;
	void setscore(int x = 2)
	{
		a = x;
	}
	void show()
	{
		cout << "The score is: " << a << endl;
	}
};

int main()
{
	score s[2];
	s[0].setscore(1);
	s[1].setscore(3);

	score* p;
	p = s;  //将对象score的地址赋值给p
	p->show();
	p++;    //对象指针变量加1
	p->show();

	score* q;
	q = &s[1]; 将第二个数组元素的地址赋值给对象指针变量q

	q->show();

	return 0;

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值