c语言是面向过程的,关注的是过程,分析出来解问题的步骤,通过函数调用逐步解决问题,而c++是基于面向对象的,关注的是对象,将一件事情拆分成为不同的对象,靠对象之间的交互完成。
在c语言结构体中只能定义变量,而在c++中不仅可以定义变量还可以定义函数,c++称为类,常用class来表示。
1.类和对象的基本概念
1.1类的概念
我们常说c++是面向对象的程序设计语言,面型对象程序设计认为万事万物都可以用类进行抽象,那么什么是类呢?简单的说类其实也是一种数据类型,而且是一种复杂的数据类型。只不过不同于一般的数据类型(int,char,double),该类型包含了不同的数据类型及和这些类型相关的操作。
拿现实生活中的例子来看,以泡茶系统为例,可以将系统分为多个类:1.茶杯类 2.茶叶类 3.顾客类上述类中每个类都有不同的数据(属性)和函数(行为)拿顾客类来说,顾客的选择是该类的行为,顾客选择什么样的茶杯和什么样的茶叶可以看成该类的数据。
1.2对象的概念
对象是类的一个具体实例。一个类可以创建出多个对象,因为在程序创建中只有类的实例才具有实际的行为(函数)和属性(数据)。以上述所说的泡茶举例,有三个类:茶杯类,茶叶类和顾客类。三个类就可以创建出三个不同的对象:茶杯对象,茶叶对象和顾客对象。以茶叶对象举例,一个茶叶类可以创建出多个茶叶对象,比如说对象1:茶叶类型为红茶 茶叶量为1g ,对象2:茶叶类型为绿茶 茶叶量为2g ,对象3:茶叶类型为普洱茶 茶叶量为3g,等等。一个类可以创建出多个对象。
1.3类和对象的关系
面向对象的编程方式就是根据实际需求中的特定对象类型来满足该需求。类实现了数据的集合及事务行为的封装,对象是类的具体实例。类与对象的关系更加形象的关系其实就相当于数据类型和其变量之间的关系。
类好比数据类型,而变量d就代表了对象。类是一种用户自定义数据类型,构成类的属性或者行为的数据或函数叫做类的成员。
2.类的定义格式和对象定义的方法
2.1类的定义格式
在c++中定义一个类一般格式如下:
其中class为定义类的关键字,className为类的名字,{}中为类的主体。
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量;类中的函数称为类的方法或者成员函数。
类成员具有不同的访问权限,其访问权限有三种:
(1)public:该关键字用于修饰的成员表示公有成员,该成员不仅在类内可以被访问,在类外也可以被访问,是类对外提供的接口
(2)private:修饰私有成员,私有成员在类内可以被访问,在类外是隐蔽的,体现了类的隐蔽性。
(3)protected:用该关键字修饰的成员为保护成员,保护成员对于类外是隐蔽的,但是对于该类的派生类来说,相当于公有成员,可以被访问。
类中成员函数既可以定义在类体中,也可以定义在类的实现部分。若已经在类体中定义,则不需要在类的实现部分定义。
2.2对象的定义方法
类只是一个模型一样的东西而对象则是类的具体实例,定义对象时,系统会为对象分配相应的内存空间。做个比方,类实例化出对象就像现实中使用建筑设计图来建造出房子,类就像设计图,只设计出图纸并没有实际的建筑物存在,同样类只是一个设计,实例化出的对象才能实际的存储数据。
对象的定义有三种形式:
(1)先定义类的类型,然后在定义对象
<类名><对象名列表>;
例如Car类的对象:
Car car1;
Car car2,*pcar,&car3 = car1,car[10];
(2) 定义类型的同时定义对象。其格式一般如下:
(3)不出现类名,直接定义对象。其格式一般如下:
比较上面三种方法,一般选择第一种,第三种很少选择,因为第三种方法只能定义一次对象,没有类名不能再定义对象。
2.3对象成员的表示
(1)一般对象成员用(.)运算符,如:
Car car1
car1.speed
car1.setSpeed(10);
(2)指向对象的指针成员表示用指针预算符(->)来表示,如:
Car car1,*pcar = &car1;
pc->speed;
pc->setSpeed(10);
(3)对象数组元素成语表示,如
Car car[10];
car[5].speed;
car[5].setSpeed(10);
下面进行一个小的实例演示,创建一个Car类,并定义不同的构造函数,打印并输出Car类对象的构造函数调用过程。
#include <iostream>
using namespace std;
class Car
{
public: //声明成员函数
void setSpeed(int speed);
void setWeight(int weight);
void move();
void stop()
{
cout << "car stoped" << endl;
}
private: //声明成员数据
int _speed;
int _weight;
};
void Car::setSpeed(int speed) //类外定义函数实现
{
_speed = speed;
}
void Car::setWeight(int weight)
{
_weight = weight;
}
void Car::move()
{
cout << "the " << _weight << " car is moving in " << _speed << endl;
}
int main()
{
Car car1,car2;
Car* pc = &car2;
car1.setSpeed(10);
car1.setWeight(500);
car1.move();
pc->setSpeed(20);
pc->setWeight(1000);
pc->move();
car1.stop();
pc->stop();
return 0;
}
3.构造函数和析构函数
对象是一个类的实体,类似于自然世界的万事万物都会有一个产生与销毁的过程,对象也是如此。对象通过构造函数进行初始化,代表了对象的出生,通过析构函数进行销毁,代表了对象的死亡。
3.1默认构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。构造函数的作用就是在对象创建的过程中初始化数据成员,在创建对象时系统会为该对象创建一个不带参数的构造函数,这个构造函数就叫做默认构造函数。如果自己创建了一个构造函数则系统将不会在创建新的构造函数。
构造函数创建的格式如下:
下面通过一段代码帮助我们更加直观的了解构造函数的用法:
#include <iostream>
using namespace std;
class Date
{
public:
Date() //无参构造函数
{
cout << "第一次调用构造函数" << endl;
}
Date(int, int, int); //带参数的自定义构造函数
Date(int, int); //满足函数重载的自定义构造函数
private:
int _year;
int _month;
int _day;
};
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
cout << "第二次调用构造函数" << endl;
}
Date::Date(int year, int month)
{
_year = year;
_month = month;
_day = 12;
cout << "第三次调用构造函数" << endl;
}
void main()
{
Date d1; //调用无参构造函数后面不能加(),否则会变成函数定义
Date d2(2023, 10, 10);
Date d3(2024, 11);
}
无参构造函数和全缺省构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参 构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1;
}
上面这段代码编译器能通过么?
可见编译是不会通过的,这也验证了我们上述的说法。
3.2析构函数
前面通过对于构造函数的学习我们明白了一个对象是怎么来的,那么一个对象是怎么结束的呢?
这就提到了析构函数,析构函数用来释放所创建的函数的。当一个对象需要销毁时,系统自动调用析构函数将该对象释放。
析构函数需要在其函数名前加上“~”来表示其功能正好与构造函数相反。
需要注意的是析构函数没有任何的参数和返回值,所以析构函数不能重载。
#include <iostream>
using namespace std;
class Car //定义一个car类
{
public:
Car(int, int); //自定义一个构造函数
~Car(); //自定义一个析构函数
void move();
private:
int speed;
int num;
};
Car::Car(int n, int s) //构造函数的实现
{
speed = s;
num = n;
cout << "Construct the " << num << "th car in " << speed << " speed" << endl;
}
Car::~Car() //析构函数的实现
{
cout << "Deconstruct the " << num << "th car" << endl;
}
void Car::move()
{
cout << "the num " << num << "th car is moving with " << speed << endl;
}
void main()
{
Car car1(1, 10), car2(2, 30); //定义两个不同的对象
car1.move(); //调用成员函数
car2.move();
}
程序运行结构如下:
在主函数中我们并没有去调用析构函数但程序仍然运行了析构函数的代码,同时从输出结果看出,对象是按照先定义先创建的原则。在程序结束时,先定义的后析构,按照后进后出的原则。
4.拷贝构造函数
拷贝构造函数的目的就是使已经存在的同类型对象来初始化创建对象的构造函数。在实际的编程中常常设计将一些数据从一个内存区域复制到另一个区域,如果复制的是基本数据类型变量,那么通过一些简单的赋值语句就可以实现,但是如果遇到需要赋值大量的成员数据的对象呢?这时候拷贝构造函数就发挥了其作用。通过拷贝构造函数可以实现将已经创建对象的私有数据成员赋值给创建对象。
4.1默认的拷贝构造函数
拷贝构造函数是一种特殊的构造函数,它也有默认拷贝构造函数,默认拷贝构造函数结构如下所示:
一般来说,如果程序员没有定义拷贝构造函数,系统会自动创建一个默认的拷贝构造函数。
实现代码:
class Car
{
public:
Car(int s) //创建一个构造函数
{
speed = s;
}
~Car() //创建一个析构函数
{
}
void Print()
{
cout << "the speed is:" << speed << endl;
}
private:
int speed;
};
void main()
{
Car car1(100);
Car car2 = car1;
car2.Print();
}
上述代码运行结果:
从输出结果看通过调用默认拷贝构造函数实现了数据成员的赋值。
4.2深拷贝和浅拷贝
在编程中,深拷贝和浅拷贝是用于复制对象的两种方式。它们的区别在于系统调用拷贝构造函数的时候,除了将成员数据拷贝过来外,是不是将一个对象动态申请的资源也拷贝过来。以下是对两种拷贝的详细解释:
1.浅拷贝
定义:浅拷贝创建一个新对象,但它并不递归地复制对象中的可变对象。相反,它只复制对象中的引用,因此新对象和原对象中的可变对象指向同一内存地址。
特点:
- 复制的是对象的引用,而不是对象本身。
- 对于不可变对象(如元组、字符串等),拷贝后不会有影响,因为这些对象不能被修改。
- 对于可变对象(如列表、字典等),如果在新对象上修改这些可变对象,则会影响到原对象,因为它们指向同一个内存位置。
2.深拷贝
定义: 深拷贝创建一个新对象,同时递归地复制所有对象中包含的可变对象。这意味着原对象和新对象在内存中是完全独立的,修改其中一个不会影响另一个。
特点:
- 复制的是对象及其所有嵌套的可变对象。
- 适用于需要完全独立的副本的场景。
- 深拷贝在处理复杂对象(如包含嵌套列表或字典的对象)时更加安全。
总结
- 浅拷贝 适用于简单对象或不需要深层独立副本的场景。
- 深拷贝 适用于需要完整独立副本的复杂对象,但可能会消耗更多内存和处理时间,因为需要复制所有嵌套对象。
4.3默认拷贝构造函数的问题
当一个对象中含有动态申请的内存空间,并用该对象初始化另一个对象,当这两个对象都被销毁的时候会出现什么情况呢?这时由于默认拷贝构造函数是一种浅拷贝,并不会拷贝动态分配的内存空间,这就造成了内存的非法访问。
5.静态成员
一个类可以创建一个或多个对象,这些对象之间相互独立,如果想实现对象之间的数据共享,就可以用到静态成员。
静态成员又可以分为静态数据成员和静态函数成员。静态成员的特点是所有实例共享同一份静态成员数据,修改静态成员的值会影响所有实例,无论创建多少个类的实例,静态成员都只会存在一份,且所有实例共享这份静态数据。
5.1静态成员
静态数据成员是 C++ 中类的一种特殊成员变量,它们与类本身相关,而不是与类的实例(对象)相关。静态数据成员的所有实例共享同一份数据,具体来说,它们在内存中只占用一份空间。
静态数据成员的特点
-
共享性:所有对象实例共享同一份静态数据成员的值。即使创建多个对象,这个静态数据成员的值也不会复制,而是所有对象共享。
-
类级别的存储:静态数据成员的存储与类本身相关,而不是类的实例。这意味着静态数据成员在类加载时分配内存,而不是在创建对象时分配。
-
通过类名访问:静态数据成员可以通过类名直接访问,而不需要实例化对象。虽然也可以通过对象访问,但推荐通过类名访问以增强代码的可读性。
-
初始化方式:静态数据成员必须在类外进行初始化,且只能被初始化一次。通常在类的定义后进行初始化。
-
访问限制:静态数据成员可以是公有的(public),私有的(private)或保护的(protected),其访问权限与普通数据成员相同。
静态数据成员的定义是在数据类型前加static关键字
静态数据成员的初始化方式实在类外进行的,其一般格式为:
<数据类型> <类名>::<静态数据成员名>=<初始值>
下面是C++中一个静态数据成员的例子:
#include <iostream>
using namespace std;
class Car
{
public:
Car(int ia);
void Showdata();
static int a;
private:
int b;
static int c;
};
Car::Car(int ia)
{
b = ia;
c = a + b;
}
void Car::Showdata()
{
cout << a << " " << b << " " << c << endl;
}
int Car::a = 0;
int Car::c = 1;
int main()
{
Car car1(10);
car1.Showdata();
Car::a = 5;
Car car2(10);
car1.Showdata();
car2.Showdata();
return 0;
}
编译,运行的结果如下所示:
5.2静态成员函数
在C++中,静态成员函数是类的特殊成员函数,它的特点是属于类本身而不是类的任何对象。其定义格式如下:
static <返回类型> <成员函数名>(<参数列表>);
和静态数据成员不同的是,静态成员函数的实现可以放在类内,也可以放在类外。静态成员函数是所有类都可以共享的。
静态成员函数只能访问静态成员变量和静态成员函数。它不能访问非静态成员变量或非静态成员函数,因为非静态成员是与具体对象关联的。
#include <iostream>
#include <cstring>
using namespace std;
class Student
{
public:
Student(char* pn, int s);
static double average();
void Addtosum();
private:
int score;
char name[10];
static int sum, count;
};
int Student::count = 0;
int Student::sum = 0;
Student::Student(char* pn, int s)
{
score = s;
strcpy(name, pn);
}
void Student::Addtosum()
{
sum += score;
count = count + 1;
}
double Student::average()
{
return sum * 1.0 / count;
}
int main()
{
Student Stu[3] = { Student("1001",80),Student("1002",85),Student("1003",92) };
for (int i = 0;i < 3;i++)
{
Stu[i].Addtosum();
}
cout << "the average score is :" << Student::average() << endl;
return 0;
}
6.常函数
在 C++ 中,“常成员”(或“常量成员”)通常是指类中的常量成员函数和常量成员变量。它们用于提供更严格的约束,以确保对象的状态在某些情况下不被修改。
6.1常成员变量
常量成员变量是在类中声明为 const
的数据成员。一旦被初始化,其值就不能被修改。常量成员变量必须在构造函数的初始化列表中进行初始化。
#include <iostream>
using namespace std;
class Car
{
public:
Car(int s);
void print();
private:
const int speed; //常成员变量
static const int w; //静态常成员变量
};
Car::Car(int s)
:speed(s) //speed只能在构造函数初始化列表中进行初始化
{
cout << "init in construction function" << endl;
}
void Car::print()
{
cout << speed << " " << w << endl;
}
const int Car::w = 100; //w可以在类外进行初始化操作
int main()
{
Car car1(60);
Car car2(70);
car1.print();
car2.print();
return 0;
}
6.2常成员函数
常量成员函数是指在成员函数的声明后面加上 const
关键字。这样的函数承诺不修改类的任何成员变量(除非它们是 mutable
的)。这在需要保证对象状态不变的情况下非常有用。其格式如下:
常成员函数可以引用const数据成员,也可以引用非const数据成员。常对象或者非常对象都可以调用const成员函数。
6.3常对象
常量对象是指被声明为 const
的对象,它的对象的数据成员的值在对象被调用时不能被修改,也就是说常对象的数据成员值只具有“读属性”。不能通过该对象调用任何非常量成员函数。只有常量成员函数可以通过常量对象调用。常对象的声明格式如下:
class calender
{
public:
calender(int y, int m, int d);
void print(); //非常成员函数
void print() const; //常成员函数 构成重载
private:
int year;
int month;
int day;
};
calender::calender(int y, int m, int d)
{
year = y;
month = m;
day = d;
}
void calender::print()
{
cout << "in non-const print function" << endl;
cout << year << " " << month << " " << day << " " << endl;
}
void calender::print() const //常成员函数实现
{
cout << "in const print function" << endl;
cout << year << " " << month << " " << day << " " << endl;
}
int main()
{
calender c1(2024, 10, 14);
const calender c2(2024, 10, 13);
c1.print();
c2.print();
return 0;
}