一、面向过程和面向对象初步认识
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事拆分成不同的对象,靠对象之间的交互完成。
二、类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:用C语言实现栈,结构体中只能定义变量;现在用C++方式实现,会发现struct中也可以定义函数。
typedef int DataType;
struct Stack
{
void Init(size_t capacity)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(const DataType& data)
{
// 扩容
_array[_size] = data;
++_size;
}
DataType Top()
{
return _array[_size - 1];
}
void Destroy()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
DataType* _array;
size_t _capacity;
size_t _size;
};
int main()
{
Stack s;
s.Init(10);
s.Push(1);
s.Push(2);
s.Push(3);
cout << s.Top() << endl;
s.Destroy();
return 0;
}
上面结构体的定义,在C++中更喜欢用class来代替。
三、类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,classname是类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。
类的两种定义方式:
①声明和定义全部放在类体中,需注意:成员函数如果在类体中定义,编译器可能会将其当成内联函数处理。
②类声明放在.h文件中,成员函数定义在.cpp文件中,注意:成员函数名前需要加类型::
一般情况下,更期望采用第二种方式。下面为了方便演示使用第一种方式。
成员变量命名规则建议:
// 我们看看这个函数,是不是很僵硬?
class Date
{
public:
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?
year = year;
}
private:
int year;
};
// 所以一般都建议这样
class Date
{
public:
void Init(int year)
{
_year = year;
}
private:
int _year;
};
// 或者这样
class Date
{
public:
void Init(int year)
{
mYear = year;
}
private:
int mYear;
};
// 其他方式也可以的,一般都是加个前缀或者后缀标识区分就行
四、类的访问限定符即封装
1、访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
①public修饰的成员在类外可以直接被访问
②protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
③访问权限的作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
④如果后面没有访问限定符,作用域就到 }即类结束
⑤class的默认访问权限为private,struct为public
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
2、封装
面向对象的三大特性:封装、继承、多态。
在类和对象阶段,主要研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、键盘、显示器、USB插孔等,让用户和计算机进行交互,完成日常事务。实际上电脑真正工作的是CPU、显卡、内存等一些硬件原件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在C++中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
五、类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Person
{
public:
void showInfo();//显示基本信息
public:
char* _name;//姓名
char* _sex;//性别
int _age;//年龄
};
//这里需要指明showInfo是属于Person类域的
void Person::showInfo()//显示基本信息
{
cout << _name << " " << _sex << " " << _age << endl;
}
六、类的实例化
用类类型创建对象的过程,称为类的实例化。
①类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。比如:类就像是一张设计图,规划了对象应该是的具体细节,但是并没有实际占用空间。当类实例化出了对象,就相当于是用设计图建出了房子,此时才会实际占用空间。
②一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储成员变量。
七、类对象模型
class Person
{
public:
void showInfo()
{
cout << _name << " " << _sex << " " << _age << endl;
}
public:
char* _name;
char* _sex;
int _age;
};
问题:类中既有成员变量,又有成员函数,那么一个类对象中包含了什么?如何计算一个类的大小?
类对象的存储方式猜测:
①对象中包含类的各个成员
这个猜测看起来是最合理的,实际上有很大的缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照这种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同的代码保存了多次,浪费空间。打个比方,就相当于是小区里本来有一个公共的篮球场就够了,上面这种方法就相当于在每家都建一个篮球场。那么如何解决这个问题呢?
②成员函数的代码只保存一份,在对象中保存存放成员函数代码的地址
③只保存成员变量,成员函数存放在公共的代码段
对于上述三种存储方式,计算机到底是按照哪种方式来存储的?
通过对下面的不同对象分别获取大小来分析看下
class A1//只有成员函数
{
public:
void fucn1() {};
};
class A2//只有成员变量
{
public:
int _a1;
};
class A3//既有成员函数又有成员变量
{
public:
void fucn1() {};
public:
int _a2;
};
class A4//空类
{
};
int main()
{
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(A3) << endl;
cout << sizeof(A4) << endl;
return 0;
}
结论:一个类的大小,实际就是该类中“成员变量”之和,注意,类也是有内存对齐规则的
空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
八、this指针
1、this指针的引出
先定义一个日期类
class Date
{
public:
void Initial(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
public:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Initial(2023, 8, 1);
d2.Initial(2023, 7, 30);
d1.Print();
d2.Print();
return 0;
}
对于上述类,有这样一个问题:
Date类中有Initial和Print两个成员函数,函数体中没有关于不同对象的区分(我们在C语言中会会传一个指向结构体变量的指针),那当d1调用Initial函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即不需要用户来传递,编译器自动完成。
2、this指针的特性
①this指针的类型:类的类型* const,即成员函数中,不能给this指针赋值。
②只能在成员函数内部使用。
③this指针本质上是成员函数的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
④this指针是成员函数第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器自动传递,不需要用户传递。
3、C语言和C++实现Stack的对比
①C语言实现
typedef int DataType;
typedef struct Stack
{
DataType* array;
int capacity;
int size;
}Stack;
void StackInit(Stack* ps)
{
assert(ps);
ps->array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == ps->array)
{
assert(0);
return;
}
ps->capacity = 3;
ps->size = 0;
}
void StackDestroy(Stack* ps)
{
assert(ps);
if (ps->array)
{
free(ps->array);
ps->array = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
void CheckCapacity(Stack* ps)
{
if (ps->size == ps->capacity)
{
int newcapacity = ps->capacity * 2;
DataType* temp = (DataType*)realloc(ps->array,
newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
ps->array = temp;
ps->capacity = newcapacity;
}
}
void StackPush(Stack* ps, DataType data)
{
assert(ps);
CheckCapacity(ps);
ps->array[ps->size] = data;
ps->size++;
}
int StackEmpty(Stack* ps)
{
assert(ps);
return 0 == ps->size;
}
void StackPop(Stack* ps)
{
if (StackEmpty(ps))
return;
ps->size--;
}
DataType StackTop(Stack* ps)
{
assert(!StackEmpty(ps));
return ps->array[ps->size - 1];
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->size;
}
int main()
{
Stack s;
StackInit(&s);
StackPush(&s, 1);
StackPush(&s, 2);
StackPush(&s, 3);
StackPush(&s, 4);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackPop(&s);
StackPop(&s);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackDestroy(&s);
return 0;
}
可以看到。在用C语言实现时,Stack相关操作函数有以下特性:
- 每个函数的第一个参数都是Stack*
- 函数中必须要对第一个参数检测,因为该参数可能会为NULL
- 函数中都是通过Stack*参数操作栈的
- 调用是必须传递Stack结构体变量的地址
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方法是分离开的,而且实现上相当复杂,涉及到大量指针操作,稍不注意可能就会出错。
②C++实现
typedef int DataType;
class Stack
{
public:
void Init()
{
_array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = 3;
_size = 0;
}
void Push(DataType data)
{
CheckCapacity();
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
return;
_size--;
}
DataType Top() { return _array[_size - 1]; }
int Empty() { return 0 == _size; }
int Size() { return _size; }
void Destroy()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
void CheckCapacity()
{
if (_size == _capacity)
{
int newcapacity = _capacity * 2;
DataType* temp = (DataType*)realloc(_array, newcapacity *
sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
_array = temp;
_capacity = newcapacity;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Pop();
s.Pop();
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Destroy();
return 0;
}
C++中通过类可以将数据以及操作数据的方法进行完美结合,通过访问权限可以控制哪些方法在类外可以被调用,即封装,在使用是就像使用自己的成员一样,更符合人类对于一个事物的认知。而且每个方法不需要传递Stack*的参数了,编译器编译之后该参数会自动还原,即C++中Stack*参数是编译器维护的,C语言中需要用户自己维护。
九、类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
然而空类中并不是什么都没有,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
十、构造函数
1、概念
class Date
{
public:
void Initial(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
public:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Initial(2023, 8, 1);
d2.Initial(2023, 7, 30);
d1.Print();
d2.Print();
return 0;
}
对于Date类,可以通过Initial公有方法给对象设置日期,如果每次创建对象都都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
2、特性
构造函数是特殊的构造函数,需要注意的是:构造函数虽然名称叫构造,但是构造函数主要任务并不是并不是开空间创建对象,而是初始化对象。
其特性如下:
①函数名与类名相同
②没有返回值
③对象实例化时编译器自动调用对应的构造函数
④构造函数不能自己调用
void Test1()
{
Date d1;
d1.Date(1,2,3);
}
⑤构造函数可以重载
class Date
{
public:
Date()//没有参数的构造函数
{
_year = 1978;
_month = 1;
_day = 1;
}
Date(int year, int month, int day)//有多个参数的构造函数
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test1()
{
Date d1;//对象实例化时,调用无参的构造函数
//注意,通过无参构造函数构造对象时,不能这么写 Date d1();
//因为这样写就成了函数声明了
cout << d1._year << " " << d1._month << " " << d1._day << endl;
Date d2(2023, 8, 2);//调用有参数的构造函数
cout << d2._year << " " << d2._month << " " << d2._day << endl;
}
void Test1()
{
Date d1;//对象实例化时,调用无参的构造函数
//注意,通过无参构造函数构造对象时,不能这么写 Date d1();
//因为这样写就成了函数声明了
cout << d1._year << " " << d1._month << " " << d1._day << endl;
Date d2(2023, 8, 2);//调用有参数的构造函数
cout << d2._year << " " << d2._month << " " << d2._day << endl;
Date d3();//尝试用这种方式看看能不能实例化对象并初始化
}
实验后证明这句代码能正常运行,不会报错,但是,这并不是实例化对象,而是函数声明。
如果你想要打印d3里的信息,就会报错:
cout << d3._year << " " << d3._month << " " << d3._day << endl;
⑥如果用户没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成
class Date
{
public:
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test1()
{
Date d1;//能够实例化对象,因为编译器会自动生成构造函数
d1.Print();
}
打印d1的信息后发现,编译器自动生成的默认构造函数不会处理d1的数据。
void Test1()
{
Date d2(2023, 7, 22);//会报错,因为默认生成的构造函数是无形参的
}
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test1()
{
Date d1;//此时会报错,因为显示定义了构造函数,编译器就不会自动生成构造函数
Date d2(2023, 7, 22);//此时正常运行
}
⑦不显式定义构造函数的情况下,编译器会自动生成默认的构造函数。但是默认生成的构造函数好像又没什么用?d1调用了编译器自动生成的默认构造函数,但是d1对象的_year/_month/_day依旧是随机值。也就是说编译器默认生成的构造函数并没有什么用??
然而事实并非如此:C++把类型分成内置类型与(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如int、char、double...,自定义类型就是我们使用class、struct、union等自己定义的类型。看看下面这个例子就会发现编译器生成的默认构造函数会对自定义类型成员变量调用它的默认构造函数。
class Time
{
public:
Time()
{
cout << "Time()" << endl;//如果调用了构造函数就会打印这句话
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
Time _time;
};
void Test1()
{
Date d1;
}
注意:C++11针对内置类型成员不初始化的缺陷,打了补丁,即:内置类型成员变量在类声明中可以给缺省值。
class Time
{
public:
Time()
{
cout << "Time()" << endl;//如果调用了构造函数就会打印这句话
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
//缺省值
int _year = 1978;
int _month = 1;
int _day = 1;
Time _time;
};
void Test1()
{
Date d1;
d1.Print();
}
⑧无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造只能有一个,注意:无参构造函数、全缺省构造函数、我们没有写编译器默认生成的构造函数,都可以认为是默认构造函数。
class Date
{
public:
Date()
{
_year = 1978;
_month = 1;
_day = 1;
}
Date(int year = 1978, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test1()
{
Date d1;//能正常运行吗
d1.Print();
}
十一、析构函数
1、概念
与构造函数功能相反,析构函数不是完成对对象本身的销毁,而是对象在销毁时会自动调用析构函数,完成对对象中数据的清理工作。
2、特性
①析构函数名是在类名前加上字符 ~ 。
②析构函数没有参数也没有返回值。
③一个类只能有一个析构函数,析构函数不能重载。若未显示定义,系统会自动生成默认的析构函数。
④对象生命周期结束时,C++编译器系统自动调用析构函数。如果一个类的成员变量有自定义类型,析构函数不仅会执行析构函数里写的代码,还会调用自定义类型的析构函数。如果定义了多个对象,后定义的对象先调用析构函数。
class Stack
{
public:
Stack(size_t capacity = 4)//构造函数
{
DataType* tmp = (DataType*)malloc(sizeof(DataType) * capacity);//动态开辟的空间一定要手动释放
assert(tmp);
_arr = tmp;
_capacity = capacity;
_size = 0;
}
void Push(DataType x)//插入
{
_arr[_size] = x;
_size++;
}
//其他方法......
~Stack()//析构函数
{
free(_arr);//释放空间
_arr = NULL;
_capacity = 0;
_size = 0;
cout << "~Stack()" << endl;//验证是否会自动调用
}
private:
DataType* _arr;
int _size;
int _capacity;
};
void Test2()
{
Stack st;
st.Push(1);
st.Push(2);
}
⑤如果我们自己没有写析构函数,编译器会自动生成析构函数,自动生成的析构函数只会调用自定义类型的析构函数。
class Time
{
public:
Time(int hour=0,int minute=0,int second=0)//构造函数
{
_hour = hour;
_minute = minute;
_second = second;
}
~Time()//析构函数
{
_hour = 0;
_minute = 0;
_second = 0;
cout << "~Time()" << endl;//验证编译器自动生成的析构函数是否调用自定义类型的析构函数
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
//没有定义析构函数
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
Time _time;//自定义类型
};
void Test1()
{
Date d1;
}
⑥如果类中没有申请资源时(例如没有动态开辟内存),析构函数可以不写,直接使用编译器自动生成的默认构造函数。
十二、拷贝构造函数
1、概念
在现实生活中,可能存在两个长得一模一样的人,我们称之为双胞胎。
那么在创建对象的时候,是否也可以创建一个与已存在的对象一模一样的新对象呢?
拷贝构造函数:只有单个参数,该形参是本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
2、特征
①拷贝构造函数是构造函数的一个重载形式。
②拷贝构造函数的参数只有一个且必须是引用,如果使用传值的方式编译器会直接报错,因为会引发无穷递归。
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
//Date(const Date d) 错误的写法
Date(const Date& d)//正确的写法
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test1()
{
Date d1(2023,3,7);
//拷贝构造函数是在实例化对象时使用
Date d2(d1);//拷贝构造的第一种用法
Date d3 = d2;//拷贝构造的第二种用法
d1.Print();
d2.Print();
d3.Print();
}
为什么值传参会导致无穷递归,而引用传参就不会呢?
③如果没有显式定义拷贝构造函数,编译器会生成默认的拷贝构造函数。对于内置类型的成员变量,自动生成的拷贝构造函数会对对象按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝,对于自定义类型的成员变量,自动生成的拷贝构造函数会调用它的拷贝构造函数。
class Time
{
public:
Time(int hour=0,int minute=0,int second=0)//构造函数
{
_hour = hour;
_minute = minute;
_second = second;
}
Time(const Time& t)//拷贝构造函数
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time(const Time& t)" << endl;//如果调用了就打印
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
//没有写拷贝构造函数
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
Time time;
};
void Test1()
{
Date d1(2023,3,7);
Date d2(d1);
d1.Print();
d2.Print();
}
④编译器自动生成的拷贝构造函数可以完成字节序的值拷贝,这是浅拷贝,但是在一些情况下,浅拷贝会出问题的如下:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 4)//构造函数
{
DataType* tmp = (DataType*)malloc(sizeof(DataType) * capacity);//动态开辟的空间一定要手动释放
assert(tmp);
_arr = tmp;
_capacity = capacity;
_size = 0;
}
void Push(DataType x)//插入
{
_arr[_size] = x;
_size++;
}
//其他方法......
~Stack()//析构函数
{
free(_arr);//释放空间
_arr = NULL;
_capacity = 0;
_size = 0;
cout << "~Stack()" << endl;//验证是否会自动调用
}
private:
DataType* _arr;
int _size;
int _capacity;
};
void Test2()
{
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
Stack st2(st1);
}
上面这段代码运行后会崩溃,
面对这种情况我们应该用在拷贝构造中实现深拷贝:
Stack(const Stack& st)
{
//深拷贝
DataType* tmp = (DataType*)malloc(sizeof(DataType)*(st._capacity));//开辟一块同样大小的空间
assert(tmp);
_arr = tmp;
memcpy(_arr, st._arr, sizeof(DataType) * st._size);//拷贝内容
_capacity = st._capacity;
_size = st._size;
}
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
注意:类中如果没有涉及到资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,拷贝构造函数是一定要写的,否则就是浅拷贝。
⑤拷贝构造函数典型调用场景:
- 使用已存在的对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值会类类型对象
十三、赋值运算符重载
1、运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,函数名字以及参数列表、返回值类型与普通函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型: 返回值类型 operator操作符(参数列表)。
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置类型的整形+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数少1个,因为成员函数(之后可能会在类外声明定义,此时就不是成员函数了,是全局函数)的第一个参数为隐藏的this指针
- .* :: sizeof ?: . 以上五个操作符是不能重载的
四个不能改变:
- 不能改变运算操作数的个数
- 不能改变运算符原有的优先级
- 不能改变运算符原有的结合性
- 不能改变运算符原有的语法结构
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
//bool operator==(Date* this, const Date& d) 成员函数的第一个参数为this指针
bool operator==(const Date& d)//实现Date==Date
{
return _year == d._year && _month == d._month && _day == d._day;
}
bool operator!=(const Date& d)//实现Date!=Date
{
return !(*this == d);//复用上面实现的Date==Date
}
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test1()
{
Date d1(2023,3,7);
Date d2(d1);
Date d3(2023, 5, 22);
cout << (d1 == d2) << endl;
cout << (d1 == d3) << endl;
cout << (d1 != d2) << endl;
cout << (d1 != d3) << endl;
}
2、赋值运算符重载
①赋值运算符重载格式
- 参数类型:constT&,传引用可以提高效率
- 返回值类型:T&,返回值传引用可以提高效率,有返回值的目的是为了支持连续赋值
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test1()
{
Date d1(2023, 3, 7);
Date d2(1970, 1, 1);
Date d3(2023, 5, 22);
d1 = d3 = d2;//连续赋值
d1.Print();
d2.Print();
d3.Print();
}
②赋值运算符只能重载成类的成员函数,不能重载成全局函数
// 赋值运算符重载成全局函数
//注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& d1, const Date& d2)
{
d1._year = d2._year;
d1._month = d2._month;
d1._day = d2._day;
return d1;
}
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
//如果权限是private全局函数无法访问成员变量
//为了方便演示这里改成public:
//接下来会讲的友元能够解决全局函数无法访问成员变量的问题
public:
int _year;
int _month;
int _day;
};
原因:赋值运算符重载如果不显式实现,编译器就会自动生成一个默认的。此时用户在类外自己实现了一个全局的赋值运算符重载,就和编译器自动生成在类中自动生成的默认的赋值运算符重载冲突了。所以赋值运算符重载函数只能是类的成员函数。
③用户没有显式定义时,编译器会自动生成一个默认的赋值运算符重载,对于内置类型,以值的方式逐字节拷贝,对于自定义类型,会调用对应的赋值运算符重载完成赋值。
class Time
{
public:
Time(int hour=0,int minute=0,int second=0)//构造函数
{
_hour = hour;
_minute = minute;
_second = second;
}
Time& operator=(Time& t)//Time类的赋值运算符重载
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time& operator=(Time& t)" << endl;
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
//没有定义Date类的赋值运算符重载
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
Time _time;
};
void Test1()
{
Date d1(2023, 3, 7);
Date d2(1970, 1, 1);
Date d3(2023, 5, 22);
d1 = d3 = d2;//连续赋值
d1.Print();
d2.Print();
d3.Print();
}
编译器自动生成的赋值运算符重载函数和上面所讲的拷贝构造函数有相同的问题,那就是浅拷贝处理指针指向动态开辟的空间时会出错,所以我们此时也是需要深拷贝
Stack& operator=(Stack& st)//Stack类的赋值运算符重载
{
//深拷贝
DataType* tmp = (DataType*)realloc(_arr, sizeof(DataType) * (st._capacity));//把空间改成和st一样大
assert(tmp);
_arr = tmp;
memcpy(_arr, st._arr, sizeof(DataType) * st._size);//拷贝内容
_capacity = st._capacity;
_size = st._size;
return *this;
}
赋值运算符重载与拷贝构造的区别:
- 赋值运算符和拷贝构造函数最大区别是赋值运算符没有新的对象生成,而拷贝构造函数会生成新的对象。
- 拷贝构造函数 是在对象创建时调用的,而 赋值 函数只能被已经存在的对象调用。
3、前置++与后置++重载
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
Date& operator++()//实现前置++
{
_day += 1;
return *this;
}
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)//实现后置++
{
Date d(*this);
++_day;
return d;
}
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test1()
{
Date d1(2023, 3, 7);
Date d2(1970, 1, 1);
Date d3 = d1++;
++d2;
d1.Print();
d2.Print();
d3.Print();
}
十四、日期类的实现
Date类的实现 · 8ba81f6 · 吕世雄/C语言 - Gitee.com
十五、const成员
用const修饰的成员函数称之为const成员函数,const修饰成员函数,实际修饰的是成员函数隐含的this指针(const Date* this),表示该成员函数中不能对任何成员进行修改。
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year;
int _month;
int _day;
};
void Test1()
{
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
}
十六、取地址及const取地址操作符重载
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。
十七、初始化列表
1、构造函数赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)//构造函数
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
上述构造函数调用之后,对象中的成员变量已经有了一个初始值,但是构造函数中的语句只能将其称为赋初始值,并不是成员函数定义的地方。
2、初始化列表
使用格式如下:
class Date
{
public:
Date(int year = 1978, 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;
};
void Test1()
{
Date d(2022,1,3);
d.Print();
}
初始化列表是成员变量定义的地方。
①每个成员变量在初始化列表中只能出现一次(定义只能定义一次)
②类中包含以下成员,必须放在初始化列表位置进行初始化:
- const成员变量
- 引用成员变量
- 自定义成员类型(且该类没有默认构造函数时)
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 0)//构造函数
{
_hour = hour;
_minute = minute;
_second = second;
}
void Print()
{
cout << "Time=" << _hour << "/" << _minute << "/" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1,
int hour = 0, int minute = 0, int second = 0)//构造函数
//初始化列表是成员函数定义的地方
:_time(hour, minute, second)//定义一个对象
, _x(33)//定义const常量
, _ryear(_year)//定义引用
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Date=" << _year << "/" << _month << "/" << _day << endl;
_time.Print();
cout << "_x=" << _x << endl;
cout << "_ryear=" << _ryear << endl;
}
private:
//这里是成员变量声明的地方
int _year;
int _month;
int _day;
Time _time;//自定义类型只有在定义时能调用其构造函数
const int _x;//const修饰的常量必须在定义时初始化
int& _ryear;//引用也必须在定义时初始化
};
void Test1()
{
Date d(2023, 8, 6, 19, 37, 56);
d.Print();
}
③对于成员变量,即使没有在初始化列表里初始化,也会在初始化列表里定义,如果这个成员变量是内置类型,就不作处理(给随机值),如果是自定义类型,就去调用它的默认构造函数
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 0)//构造函数
{
_hour = hour;
_minute = minute;
_second = second;
}
void Print()
{
cout << "Time=" << _hour << "/" << _minute << "/" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1,
int hour = 0, int minute = 0, int second = 0)//构造函数
//初始化列表是成员函数定义的地方
: _x(33)//定义const常量
, _ryear(_year)//定义引用
{
//_year不在初始化列表初始化,也不在构造函数内初始化
//那么就是随机值,_ryear也应该是随机值
//_time会调用它的默认构造函数
_month = month;
_day = day;
}
void Print()
{
cout << "Date=" << _year << "/" << _month << "/" << _day << endl;
_time.Print();
cout << "_x=" << _x << endl;
cout << "_ryear=" << _ryear << endl;
}
private:
//这里是成员变量声明的地方
int _year;
int _month;
int _day;
Time _time;//自定义类型只有在定义时能调用其构造函数
const int _x;//const修饰的常量必须在定义时初始化
int& _ryear;//引用也必须在定义时初始化
};
void Test1()
{
Date d(2023, 8, 6, 19, 37, 56);
d.Print();
}
④类中成员变量声明的顺序就是初始化列表中成员变量定义的顺序,而不是按照我们写的顺序定义。
看下面这段代码,请问_a1和_a2的打印结果是多少?
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
void Test3()
{
A a(13);
a.Print();
}
因为_a2先声明,所以_a2也会先定义,_a2(_a1)时,_a1还没有定义,所以_a1是随机值,所以此时_a2的值也是随机值。然后_a2定义,_a1等于了13。所以最后的打印结果应该是 13 随机值。
十八、隐式类型转换
1、关于隐式类型转换
C++支持单参数构造函数的隐式类型转换。
class A
{
public:
A(int a)
{
_a1 = a;
}
void Print() {
cout << _a1 << endl;
}
private: int _a1;
};
void Test3()
{
A a = 13;
//编译器实际进行的操作应该是:
//先调用构造函数,用13创建一个临时对象,
//再用临时对象拷贝构造a,
//但是编译器对于一个表达式中,连续构造+拷贝构造时会进行优化,
//直接用13来构造a。
a.Print();
}
看下面这段代码,为什么会报错:
A& a2 = 13;
因为隐式类型转换时,会生成临时对象,这个临时对象是常量,让a2引用它,就是权限放大了,权限是不能放大的,所以这里会报错。加上const就行了
const A& a2 = 13;
因为上面这段代码只拿13去构造了临时对象,所以编译器不会进行优化。
C++11支持了多参数构造函数的隐式类型转换:
class A
{
public:
A(int a1, int a2)
{
_a1 = a1;
_a2 = a2;
}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
void Test3()
{
A a = { 13,66 };//多参数
a.Print();
}
2、explicit关键字
使用explicit关键字可以禁止构造函数的隐式类型转换:
class A
{
public:
explicit A(int a1, int a2)
{
_a1 = a1;
_a2 = a2;
}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
void Test3()
{
A a = { 13,66 };//此处会报错,因为禁止构造函数隐式类型转换了
a.Print();
}
3、隐式类型转换带来的便利
class B
{
public:
B(int b1=0, int b2=0)
{
_b1 = b1;
_b2 = b2;
}
private:
int _b1;
int _b2;
};
class A
{
public:
A()
{
_size=0;
_pb = (B*)malloc(sizeof(B) * 10);
}
~A()
{
free(_pb);
_pb = NULL;
_size = 0;
}
void Push(const B& b)//放入B类对象
{
_pb[_size++] = b;
}
private:
int _size;
B* _pb;//指向一块存放B类对象的空间
};
void Test3()
{
A a;
B b1(3, 4);
a.Push(b1);
B b2(5, 6);
a.Push(b2);
B b3(7, 8);
a.Push(b3);//这样写太过于麻烦
//可以直接这样写:
a.Push({ 9,10 });
a.Push({ 11,12 });
a.Push({ 13,14 });
}
十九、匿名对象
C++允许定义匿名对象
B b(2, 2);//有名对象
B(2, 2);//匿名对象
- 匿名类的特点就是不用取名字
- 匿名类的生命周期只有这一行
匿名类在隐式类型转换被禁用时能给我们带来便利:
class B
{
public:
explicit B(int b1=0, int b2=0)//禁止隐式类型转换
{
_b1 = b1;
_b2 = b2;
}
private:
int _b1;
int _b2;
};
class A
{
public:
A()
{
_size=0;
_pb = (B*)malloc(sizeof(B) * 10);
}
~A()
{
free(_pb);
_pb = NULL;
_size = 0;
}
void Push(const B& b)
{
_pb[_size++] = b;
}
private:
int _size;
B* _pb;
};
void Test3()
{
A a;
a.Push(B(1, 2));//匿名类
a.Push(B(1, 2));
a.Push(B(1, 2));
a.Push(B(1, 2));
}
二十、static成员
用static修饰的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数吗,称之为静态成员函数 。静态成员变量一定要在类外进行初始化。
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。
class A
{
public:
A(int a1 = 0)
{
_a1 = a1;
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
static void Test()
{
cout << "static void Test()" << endl;
}
private:
int _a1;
public:
static int _a2;
};
int A::_a2 = 3;
void Test3()
{
A a1(13);
A a2(9);
a1.Print();
a2.Print();
}
- 静态类成员可用 类名::静态成员 或者 对象.静态成员 来访问。
void Test3()
{
cout << A::_a2 << endl;
A::Test();
}
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
- 静态成员也是类的成员,受public、protected、private访问限定符的限制。
二十一、友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
1、友元函数
在重载cout<<Date时会遇到一个问题:因为隐含的this指针占了第一个形参的位置,但实际使用中输出流对象cout必须是第一个形参,才能正常使用。所以要将cout<<Date重载成全局函数,但是这又会导致在类外无法访问成员变量,此时就需要用友元来解决问题。cin>>Date同理。
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//实际为void operator<<(const Date* this, ostream& cout)
ostream& operator<<(ostream& cout)const
{
cout << _year << "/" << _month << "/" << _day;
return cout;//返回cout是为了实现连续输出
}
private:
int _year;
int _month;
int _day;
};
void Test1()
{
Date d(2023, 8, 6);
d << cout;//我们必须这样使用才行,因为this指针是第一个参数
//但是这样使用并不合理
}
友元函数可以直接访问类的私有成员,它是定义在类外的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date
{
friend ostream& operator<<(ostream& cout, const Date& d);//声明友元函数
public:
Date(int year = 1978, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& cout, const Date& d)
{
cout << d._year << "/" << d._month << "/" << d._day;
return cout;//返回cout是为了实现连续输出
}
void Test1()
{
Date d1(2023, 8, 6);
Date d2(2023, 8, 7);
cout << d1 << " " << d2;//这样就合理了
}
- 友元函数可以访问类的私有和保护成员,但不是类的成员函数。
- 友元函数不能用const修饰(因为没有隐含的this指针)。
- 友元函数可以在类定义的任何地方声明,不受访问限定符限制。
- 一个函数可以是多个类的友元函数。
2、友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问可另一个类中的非公有成员。
- 友元函数是单向的,不具有交换性
比如A类是B类的友元类,那么可以在A类中直接访问B类的私有成员变量,但是不能在B类中直接访问A类的私有成员变量。
- 友元关系不能传递
如果A是B的友元,B是C的友元,A不是C的友元。
- 友元关系不能继承
class Time
{
friend class Date;//声明Date类为Time类的友元类
public:
Time(int hour = 0, int minute = 0, int second = 0)//构造函数
{
_hour = hour;
_minute = minute;
_second = second;
}
void Print()
{
cout << "Time=" << _hour << "/" << _minute << "/" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1978, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void setTimeofDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_time._hour = hour;
_time._minute = minute;
_time._second = second;
}
private:
int _year;
int _month;
int _day;
Time _time;
};
二十二、内部类
概念:如果一个类定义在另一个类的内部,这个类就叫做内部类。内部类所在的类称为外部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类,没有任何优越的访问权限。
注意:内部类天生就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
①内部类定义在外部类的public、protected、private都是可以的。
②内部类可以直接访问外部类的static成员,不需要外部类的对象/类名。
③sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
void Test3()
{
A::B b;
b.foo(A());
}
二十三、拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还
是非常有用的。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
private:
int _a;
};
void f1(A aa)
{
}
A f2()
{
A aa;
return aa;
}
void Test3()
{
// 传值传参
A aa1;
f1(aa1);
cout << "----------------------------" << endl;
// 传值返回
f2();
cout << "----------------------------" << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
cout << "----------------------------" << endl;
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << "----------------------------" << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << "----------------------------" << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << "----------------------------" << endl;
}
完结。。。。。。。。。
标签:对象,month,int,year,Date,day,构造函数 From: https://blog.51cto.com/u_15855358/6998311