一.类的默认成员函数
二.构造函数
class Date
{
public:
//无参构造函数
Date()
{
_year = 0;
_month = 0;
_day = 0;
}
//有参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
全缺省构造函数
//Date(int year = 1, int month = 1, int day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 如果留下三个构造中的第二个带参构造,第一个和第三个注释掉
// 编译报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1; // 调用默认构造函数
Date d2(2025, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则编译器无法
// 区分这里是函数声明还是实例化对象
// 比如上面的d1我们就不能写成Date d1()。这样无法确定是定义了一个函数还是实例化对象,比如下面就会报警告
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
Date d3();
d1.Print();
d2.Print();
return 0;
}
代码里有一些注意事项:比如实例化的时候后面不能加(),全缺省构造函数不能和其他两种同时存在,我们可以只用全缺省构造函数,全缺省构造函数更加的方便(这里结合后面的6一起看)。
class Stack
{
public:
Stack(int n = 4)
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
return;
}
_capacity = n;
_top = 0;
}
private:
int* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue s1;
return 0;
}
这里对于实例化对象s1,MyQueue的默认构造系统取调用Stack的默认构造。
总结:大多数情况,构造函数需要我们自己去实现,少数情况类似上面的MyQueue切Stack有默认构造时,MyQueue自动生成就可以用。
三.析构函数
~Stack()
{
free(_a);
_a = nullptr;
_capacity = 0;
_top = 0;
}
(1~2)这也是在Stack类里写的。
class Stack
{
public:
Stack(int n = 4)
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
free(_a);
_a = nullptr;
_capacity = 0;
_top = 0;
}
private:
int* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//编译器默认生成MyQueue的析构函数调用了Stack的析构,释放的Stack内部的资源
// 显示写析构,也会自动调用Stack的析构
/*~MyQueue()
{}*/
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack s1;
return 0;
}
也就是说,如果我们没有写析构函数,系统会生成,但是系统生成的不管内置成员类型。对于自定义的类型,即使我们写了析构,但是里面的成员还是会调用析构函数,比如上面的~MyQueue()
{}。依然会调用Stack的析构函数
四.拷贝构造函数
class Date
{
public:
//全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 7, 13);
d1.Print();
Date d2(d1);//也可以写成Date d2 = d1;
d2.Print();
return 0;
}
需要注意的一点是,我们这里传参的是类类型的引用,如果我们不传这个,使用传值调用的话会报错:
本来我们是想把d1的值拷贝给d2,这里我们用的传值传参(值就是d1),就必须要调用拷贝构造函数Date d(d1)。先把d1的值拷贝给d,因为我们的拷贝构造函数是用传值传参来写的,想把d1的值拷贝给d,就必须要有d1传值的过程,既然有了传值,就要调用拷贝构造函数Date d(d1)。。。以此循环 就成了无限递归。
那么我们该如何解决呢?其实就是用引用就行了,因为引用就是别名,不会在形成新的拷贝构造了。
class Stack
{
public:
//初始化栈
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
//入栈
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
//销毁栈
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
// Stack不显示实现拷⻉构造,用自动生成的拷⻉构造完成浅拷⻉
// 会导致st1和st2里面的_a指针指向同一块资源,析构时会析构两次,程序崩溃
Stack st2 = st1;
return 0;
}
比如这里我没有写拷贝构造函数,那么系统就会默认生成拷贝构造函数,这个拷贝构造函数是浅拷贝,下图是浅拷贝最终拷贝出来的st2,我们发现st1和st2里的_a的地址相同,因为_a的内存是我们动态申请的,所以最后_a的生命周期结束了之后,编译器会自动调用析构函数,因为是两个对象,所以这一块空间在析构了一次之后,还会再析构一次,这样相当于是释放一块不存在的空间。
如果这样不行的话,对于像Stack这样有资源的空间,我们还是要自己写拷贝构造函数。
Stack& func()
{
Stack st;
return st;
}
这里我写的是一个引用返回的一个函数,在这个函数内部我创建了一个栈,然后我返回了这个栈,因为这个栈是在函数内部创建的,所以当出了这个函数之后,st的生命周期也就到头了,st就会销毁,那么返回的是个什么东西?这里就是野引用了。
五.赋值运算符重载
5.1运算符重载
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1,const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
里面的operator==就是函数名。在使用的时候我们既可以写成operator==(d1,d2)。还可以写成d1==d2因为编译器会自动转换成上面的那种形式。
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
这个函数成员函数在类里面,这时如果我们调用的话就跟上面的不一样了。这个调用的话是这样:
int main()
{
Date d1(2024, 7, 13);
Date d2(2024, 7, 13);
d2.operator==(d1);
return 0;
}
class A
{
public:
void func()
{
cout << "A::func()" << endl;
}
};
typedef void(A::* PF)(); //成员函数指针类型
int main()
{
// C++规定成员函数要加&才能取到函数指针
PF pf = &A::func;
A obj;//定义ob类对象temp
// 对象调用成员函数指针时,使用.*运算符
(obj.*pf)();
return 0;
}
这里面就是.*运算符.
5.2赋值运算符重载
class Date
{
public:
//全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
cout << " Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year <<"/"<< _month <<"/"<< _day << endl;
}
//赋值运算符重载
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
// d1 = d2表达式的返回对象应该为d1,也就是*this
return *this;
}
private:
int _year;
int _month;
int _day;
};
注意这里只有当返回值是类类型引用的时候,返回的是*this,我们才可以连续赋值,还有就是上面为什么写成了传引用,如果是传值的话会调用拷贝构造函数,多此一举了。
六.取地址运算符重载
6.1const成员函数
void Print() const
{
cout << _year <<"/"<< _month <<"/"<< _day << endl;
}
const Date d1(2024, 7, 14);
d1.Print();
因为&d1的类型是const Date*,所以如果我们用Print调用的时候,我们传参过去给this指针的话,是从Date* const 类型转换成了const Date*类型,发生了权限扩大。所以这样是错误的写法。
这里还需要注意的一点是,Date* const this 里const修饰的是谁?这里的const修饰的是this指针,限定的this指针的指向,千万不要把Date* const this与cosnt Date* this弄混淆了(后者的意思是this所指向的对象不允许改动)。
针对这一点,为了避免这样的情况发生,如果在不改变指向对象的前提,尽量在函数后面都加上const。
6.2取地址运算符重载
Date* operator&()
{
return this;
// return nullptr;
}
const Date* operator&()const
{
return this;
//return nullptr;
}
这个取地址函数重载大部分时候不需要我们自己写,如果我们不希望别人取到我们的类对象的地址,我们可以用一下这个,return一个没有用的地址给别人。
到这里类与对象(中)就差不多结束了,如有错误还请多多指出。