- 初步认识类与对象
- 一、面向过程与面向对象的区别
- 二、类与结构体
- 三、类的定义
- 四、类的实例化
- 五、类对象
- 六、this指针
- 七、构造函数
- 八、析构函数
- 九、拷贝构造函数
- 十、运算符重载函数
C语言是面向过程进行编程,注重点在过程上,这个过程指的是解决问题的过程。C++是面向对象进行编程,注重点是对象,主要处理的是对象与对象间的关系。
举个例子来了解两者之间的区别,例如送外卖这件事情。
(1)面向过程
(2)面向对象
过程:外卖员到商家店铺,商家将外卖提供给外卖员。外卖员到客户指定点,将外卖送给客户,客户拿到外卖。
对象:外卖员、外卖、商家、客户
外卖员:前往商店拿外卖,将外卖送给客户
外卖:由商家提供,先给外卖员,最后给客户
商家:提供外卖给外卖员
客户:拿到外卖员送到的外卖
在面对对象中,客户不需要知道商家是如何制作外卖的,只需要拿到外卖员送到的外卖,并确认是否是自己点的外卖。每个对象只需要关注自己有关的事情,不需要了解整个国产的实现。
在C语言中结构体由struct关键字定义。C++中,类可以由class和struct关键字定义。C++是兼容C的,因此struct依旧可以作为结构体使用。
C++和C语言的struct的区别有哪些?C++的struct用法包含C的struct的用法,也就是说C++在C的struct基础上增加了一些东西,让其从结构体变成了类。
第一个点,C++中的struct不仅可以定义成员变量,还可以定义成员函数。
第二点,C++中struct声明的是类名(用C语言写结构体可以看做结构体名),在定义变量时,可以不用加struct,可以直接用类名做变量类型。
第三点,C++中struct中的成员函数和成员变量有公有和私有之分,公有就是可以在外部进行调用,私有只能在自己的类域中使用。(struct和class声明的类被{}圈定的范围就是类域)
class和struct的定义类的方法相同,都是由于定义类的关键字。在定义上的不同点是struct可以作为结构体使用,class不行。class定义的类成员和函数,在默认情况下都是私有的;struct定义的类成员和函数,在默认情况下都是公有的。
class和struct在继承和模板参数列表位置上也不一样,在后续会进行讲解。
public和private被称为访问限定符,public为公有权限,private为私有权限。
class Stack
{public://公有
void StackInit()
{arr = nullptr;
capacity = num = 0;
}
private://私有
DataType* arr;
int capacity;
int num;
};
int main()
{Stack st;
st.StackInit();
//st.capacity = 10;//错误操作,无法访问私有变量
return 0;
}
在类中,类中的内容被称为类的成员;类中的函数被称为成员函数或者类的方法;类中的变量被称为成员变量或者成员属性。
三、类的定义类的定义大致分为两种,一种是类的成员声明和定义都放在类中,另一种是类的声明放在.h文件中,类的定义放在.cpp文件中。
1.声明和定义放类中:成员函数在类中定义可能会被编译器当成内联函数进行处理,也就是调用该函数时,不会开辟函数栈帧,而是会直接在调用处展开。
#include#includeclass Student
{public:
void Init(const char* name, int age, int score)
{strcpy(_name, name);
_age = age;
_score = score;
}
void Print()
{printf("%s %d %d", _name, _age, _score);
}
private:
char _name[20];
int _age;
int _score;
};
int main()
{Student s;
s.Init("张三", 20, 88);
s.Print();
return 0;
}
2.声明放在.h文件、定义放在.cpp文件:在函数成员定义时,需要在函数成员名前面加作用域限定符,作用域为该类的类域。一般情况下,建议使用第二种方法。
//.h文件
class Student
{public:
void Init(const char* name, int age, int score);
void Print();
private:
char _name[20];
int _age;
int _score;
};
//.cpp文件
void Student::Init(const char* name, int age, int score)
{strcpy(_name, name);
_age = age;
_score = score;
}
void Student::Print()
{printf("%s %d %d", _name, _age, _score);
}
int main()
{Student s;
s.Init("张三", 20, 88);
s.Print();
return 0;
}
为什么要加作用域限定符?因为成员函数都在类域中,出了类域是没法直接找到的。当函数前面加了作用域限定范围,就相当于可以直接找到该函数了,换句话讲,加完之后,相当于你在你家直接操控公司的电脑,那你在使用该电脑的时候,所有东西也只会在这个电脑里产生和销毁。
四、类的实例化类的实例化就是用类类型去创建此类对象的过程。
在这里讲讲类和对象的关系,类相当于一个模板。比如你要创造一辆车,首先得有车的设计图纸。通过设计图纸才能高效的制造车,一张图纸就可以批量生成这种型号的车。类就是生产图纸,对象就是车,通过类可以批量创建对象。
类是不占内存的,而对象占内存。就像图纸上画的东西没有真正实现出来,只存在纸上,不额外占空间。生成出来的车子是真实存在的,占用物理空间。
类对象既然是类的实例化,那同样拥有成员函数和成员变量的,那成员如何计算其大小呢?需不需要计算成员函数的大小?成员函数的大小是不用计算的,因为成员函数并不在类对象中,而是在代码段中。为什么要这样子?如果每一个同类型的对象都需要拥有一个属于自己的函数,对象数量多,会造成大量的空间浪费,不如放在外面,供每个对象需要时使用。
按照上述说法,计算类对象大小只需要计算成员变量的大小,那是直接将成员变量大小相加就是类对象大小?不是的,这个和成员变量的存储方式有关。成员变量的内存存储方式和结构体的相同,不明白的可以看看前面的内存对齐。那按照结构体内存对齐的方式去计算大小就一定正确吗?不一样,有一种特殊情况,就是没有成员变量,但是这不能代表这个类对象大小是0,如果类对象大小是0,就表示这个对象根本没有开辟空间,没有地址如何表示这个对象?因此在没有成员变量的情况下,类对象大小为1,需要有一个地址来表示这个类对象。
#include#includeclass Student
{public:
void Init(const char* name, int age, int score)
{strcpy(_name, name);
_age = age;
_score = score;
}
void Print()
{printf("%s %d %d", _name, _age, _score);
}
private:
char _name[20];
int _age;
int _score;
};
int main()
{Student s1;
Student s2;
s1.Init("张三", 20, 88);
s1.Print();
s2.Init("李四", 21, 89);
s2.Print();
return 0;
}
为了方便讲解,使用第一种定义方法。在上面代码中,可以看到定义了两个类变量分别叫s1和s2,在调用Student类函数时,明明没有传入任何关于类变量的信息,那成员函数是如何区分并使用对应的成员变量?也就是说成员函数如何判断是使用s1的成员变量还是s2的?这其实是C++编译器给每个非静态成员函数都多加了一个指针参数,这个指针指向的就是调用这个函数的类变量。这个指针参数名字就是this,类型是类类型加上* const(如:Student * const this),这是为了防止this改变指向对象,出现误操作。
在成员函数中,使用成员变量_name在实现上其实是this->_name。这就是为什么成员函数可以随你的心意去实现相关操作,就是因为有些你看不到的东西,在默默守护你。那this存在哪里呢?this作为形参,一般都是存在于栈里,但是有些编译器上会将其放在寄存器中。
构造函数是类中特殊的成员函数,其名字和类名相同,是在创建类对象时由编译器进行自动调用的函数。这也就是保证该函数只会在其生命周期内被调用一次。主要作用就是确保对象成员变量都有初始值。
那构造函数存在哪些特点呢?
1.函数名与类名相同
2.构建类对象时,会被编译器自动调用默认构造函数
3.没有返回值
4.构造函数可以函数重载,非默认构造函数需要自己调用
默认构造函数包括三种:(1)编译器自动生成的构造函数(2)自己定义的没有函数参数的构造函数(3)自己定义的全缺省构造函数
构造函数是默认存在的,也就是说,即使程序员没有在类中写构造函数,编码器会自己生成一个。
编译器自动生成的构造函数会具体实现哪些功能?为什么还要自己去定义构造函数?
先说明第一个问题,编译器自动生成的构造函数不会对内置类型变量进行初始化处理(不做处理),对应自定义类型变量会调用它的默认构造函数进行初始化处理。内置类型就是C++提供的数据类型,自定义类型就是自己定义的类型,比如上面用class和struct等等自己定义的类型。
第二个问题:第一个问题的结果说明了自动生成的构造函数不会对内置类型变量进行初始化,那么在存在内置类型的类中,往往需要对其进行初始化,所以需要自己定义构造函数。
#includeclass Date
{public:
Date(int year = 0, int month = 1, int day = 1)//构造函数
{_year = year;
_month = month;
_day = day;
}
void Print()
{printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{Date d1;
d1.Print();
Date d2(2001,1,1);//构造函数传参方式比较特殊,直接在对象后面加()进行
d2.Print();
return 0;
}
在自己定义构造函数的时候,需要注意以下几点:(1)函数参数能用全缺省参数,尽量用全缺省,即使不行最好也是半缺省参数(最好用缺省参数)(2)构造函数不需要返回值(3)构造函数名字与类名相同
在C++11中对于自动生成的构造函数内置类型没有初始化打了个补丁,可以在成员变量定义时给默认值作为初始化。
析构函数的作用与构造函数相反,构造函数是进行初始化,析构函数就是进行清理工作。
首先得知道析构函数什么时候会被调用?当对象生命周期结束需要销毁时,会调用析构函数先对其进行清理。那会清理什么?这和构造函数相似,析构函数对于内置类型是不做清理的,对自定义类型会调用其析构函数做清理。也就是说对于内置类型析构函数是不做任何操作的。如果说在一个管理栈的类,用编译器自动生成的析构函数,是会造成资源泄露的。举个例子,栈在使用时不可避免需要开辟空间,空间不进行释放会有什么后果应该都清楚(不清楚的可以看看前面的动态内存管理),因此在有申请资源时,最好写析构函数。
这里可能会有人觉得为什么析构函数和构造函数都没有对内置类型进行处理,怎么说呢,构造函数那里是存在问题,但是析构函数不对内置类型进行处理是考虑到有些情况下,不可以对申请的资源进行清理。比如说我申请了一块空间,但是这个空间我在外面还需要时候,不可以在对象销毁时进行清理。
析构函数的特点:
(1)函数名是"~"加上类名
(2)没有返回值且没有参数
(3)在对象生命周期结束编译器会自动调用析构函数
(4)析构函数没有函数重载,只能存在一个析构函数
#includeclass Date
{public:
Date(int year = 0, int month = 1, int day = 1)
{_year = year;
_month = month;
_day = day;
}
~Date()//析构函数
{_year = _month = _day = 0;//没有清理资源可以不写,因为这些后面会被销毁掉
}
void Print()
{printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{Date d1(2001,1,1);
d1.Print();
return 0;
}
九、拷贝构造函数拷贝构造函数只有一个函数参数,该参数类型为类的类型的引用,通常会加个const修饰。在通过已有对象创建新对象时,编译器会自动调用拷贝构造函数。
没有定义拷贝构造函数,编译器会自动生成一个。
拷贝构造函数特点:
(1)只有一个形参,并且形参类型必须为类类型的引用。
(2)是构造函数的重载形式
形参类型为什么必须是类类型的引用?如果只是用类的类型,本质上是传值调用,函数的传值本质是将该数值再拷贝一份临时变量传入函数中。这就会导致对象调用拷贝函数时,拷贝函数会对对象再次调用拷贝函数,不断循环往复陷入死循环。使用引用不会对对象进行拷贝,而是取了个别名,使用别名进行操作。通常加const是因为权限问题,const修饰的引用权限最小,其他数值都可以传入。
没有定义拷贝构造函数时,编译器会自动生成默认拷贝构造函数。默认拷贝构造函数对内置类型变量实现的是浅拷贝(或者值拷贝),就是对象的内存存储按字节序方式进行拷贝。也就是一个字节一个字节将对象拷到目标对象中。对自定义类型变量是通过调用它的拷贝构造函数进行拷贝。
那什么时候可以使用默认拷贝构造函数?在拷贝内容存在指针或者引用时,需要注意实际含义。举个例子,被拷贝的对象s1中有两个数值,一个为变量a,一个为指针pa,指针pa指向a变量。拷贝对象s2需要拷贝s1中的a和pa,那s2中的a值会与s1的相等,而s2中的pa却不是指向s2中的a,而s1中的a。这个情况和预期是有出入的,不是我们想要的结果,这时候需要自定义拷贝构造函数。
#includeclass Date
{public:
Date(int year = 0, 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;
}
~Date()
{_year = _month = _day = 0;
}
void Print()
{printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{Date d1(2001, 1, 1);
d1.Print();
Date d2(d1);
d2.Print();
return 0;
}
十、运算符重载函数为什么需要运算符重载函数?用运算符做个类比,我们在使用运算符的时候,可以实现变量之间的比较和加减乘除等等操作。但是运算符并不能对类对象以上的操作,类对象和类对象不能通过运算符直接进行运算,因此我们需要对运算符进行重载,使其可以对同类型的类对象进行运算。
在运算符重载中,赋值运算符重载函数在没有定义时,编译器会自动生成。如果类中没有进行资源管理,可以不用自己实现,否则需要自己定义赋值运算符重载函数。
运算符重载函数特点:
(1)“.”、“.*”、“::”、“?:”、"sizeof"这五个是不可以进行运算符重载的
(2)不可以改变内置类型的运算符含义
(3)返回类型 operator+“运算符”(形参)
(4)形参会比实际操作数少一个,因为第一个操作数会被作为隐藏的this指针进行传入
日期类的实现:
#includeusing std::ostream;
using std::cout;
using std::cin;
using std::endl;
class Date
{friend ostream& operator<<(ostream& out, const Date d);
public:
int Judge_day(int year, int month);
Date(int year = 0, int month = 1, int day = 1);
bool operator>(Date d);
bool operator==(Date d);
bool operator<(Date d);
bool operator>=(Date d);
bool operator<=(Date d);
bool operator!=(Date d);
Date operator+=(int day);
Date operator+(int day);
Date operator-=(int day);
Date operator-(int day);
Date& operator=(const Date d);
// 前置++
Date & operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
int operator-(const Date& d);
void Print() const;
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date d)
{out<< d._year<< "-"<< d._month<< "-"<< d._day;
return out;
}
int Date::Judge_day(int year, int month)
{int date[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = date[month];
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
{day += 1;
}
return day;
}
Date::Date(int year, int month, int day)
{_year = year;
_month = month;
_day = day;
if (!(year >= 0
&& month >0 && month< 13
&& day >0 && day<= Judge_day(year, month)))
{printf("日期错误: %d-%d-%d\n", year, month, day);
}
}
bool Date::operator>(Date d)
{if (this->_year >d._year)
{return true;
}
else if (this->_year == d._year && this->_month >d._month)
{return true;
}
else if (this->_year == d._year && this->_month == d._month && this->_day >d._day)
{return true;
}
else
{return false;
}
}
bool Date::operator==(Date d)
{if (this->_year == d._year && this->_month == d._month && this->_day == d._day)
{return true;
}
return false;
}
bool Date::operator<(Date d)
{return !(*this >d || *this == d);
}
bool Date::operator>=(Date d)
{return !(*this< d);
}
bool Date::operator<=(Date d)
{return !(*this >d);
}
bool Date::operator!=(Date d)
{return !(*this == d);
}
Date Date::operator+=(int day)
{if (day< 0)
{return *this -= (-day);
}
int i = 0;
day += this->_day;
while (i = Judge_day(this->_year, this->_month), day >i)
{day -= i;
this->_month++;
if (this->_month >12)
{ this->_month = 1;
this->_year++;
}
}
this->_day = day;
return *this;
}
Date Date::operator+(int day)
{if (day< 0)
{return *this - (-day);
}
int i = 0;
Date ret(*this);
day += ret._day;
while (i = Judge_day(ret._year, ret._month), day >i)
{day -= i;
ret._month++;
if (ret._month >12)
{ ret._month = 1;
ret._year++;
}
}
ret._day = day;
return ret;
}
Date Date::operator-=(int day)
{if (day< 0)
{return *this += (-day);
}
day = this->_day - day;
while (day<= 0)
{this->_month--;
if (this->_month< 1)
{ this->_month = 12;
this->_year--;
}
day += Judge_day(this->_year, this->_month);
}
this->_day = day;
return *this;
}
Date Date::operator-(int day)
{if (day< 0)
{return *this + (-day);
}
Date ret(*this);
day = ret._day - day;
while (day<= 0)
{ret._month--;
if (ret._month< 1)
{ ret._month = 12;
ret._year--;
}
day += Judge_day(ret._year, ret._month);
}
ret._day = day;
return ret;
}
Date& Date::operator=(const Date d)
{this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
return *this;
}
void Date::Print() const
{printf("%d-%d-%d\n", _year, _month, _day);
}
// 前置++
Date& Date::operator++()
{*this += 1;
return *this;
}
// 后置++
Date Date::operator++(int)
{Date ret(*this);
* this += 1;
return ret;
}
// 后置--
Date Date::operator--(int)
{Date ret(*this);
*this -= 1;
return ret;
}
// 前置--
Date& Date::operator--()
{*this -= 1;
return *this;
}
int Date::operator-(const Date& d)
{Date ret(d);
int i = 0;
int day = 0;
while (ret != *this)
{ret += 1;
day++;
}
return day;
}
int main()
{return 0;
}
++和–分前置和后置,如果没有用于区分的对象,无法方便是前置还是后置。因此前置被定为默认情况,如果要进行后置++或–的函数重载,需要在声明时,在参数列表处加上int,作为后置的标志,不需要变量名(因为函数不会接收这个值,只是用来判断)。
如有不当之处,请各位看客指正。
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
网站名称:C++入门(2)-类与对象-创新互联
转载来源:http://lswzjz.com/article/csegdj.html