很久以前学过C++,但是后来做嵌入式一直用C,面向对象什么的都快忘光了,写一些东西权当复习C++了
文章目录
一、类和对象相关概念
(1)面向过程的程序设计
- 面向过程的程序 = 算法+数据结构
- 程序由
全局变量
和函数
构成,用函数操作数据结构 - 不足:
- 函数和其操作的数据结构没有直观的联系,程序长了之后,难以直接看出
- 某个数据结构有哪些函数可以对它操作操作
- 某个函数到底是操作哪些数据结构的
- 任意两个函数间存在怎样的调用关系?
- 没有“封装”和“隐藏”的概念,要访问某个数据结构中的某个变量,可以直接访问。当变量定义变化时,就必须找到所有调用处修改,难以检查哪个函数导致错误
- 难以进行代码重用
- 函数和其操作的数据结构没有直观的联系,程序长了之后,难以直接看出
(2)面向对象的程序设计
-
面向对象的程序 = 类+类+…+类
-
设计方法:
- 把某类事物的客观属性(共同特点)归纳出来,形成一个数据结构(可以用多个变量描述事物的属性)
- 把这类事物能进行的行为也归纳出来,形成一个个函数,用于操作数据结构(这一步叫
抽象
) - 然后通过某种语法形式,把数据结构和操作此结构的函数捆绑到一起,形成一个“类”,从而将数据结构和函数紧密联系起来(这步叫
封装
)
-
基本特点:抽象、封装、继承、多态
-
类用class定义,和结构体形式基本一样,用类定义的变量叫
对象
-
对象的内存分配:对象占据的内存大小,等于所有成员变量大小(不包含成员函数)之和
-
对象间的运算:可以用=进行赋值,但是不能用==,!=,>,<,>=,<=这些,除非经过运算符重载
-
使用成员变量和成员函数:
- 类名.成员名
- 指针->成员名
- 引用名.成员名
二、构造函数相关概念
(1)类的定义和使用
- 类定义示例:如下程序所示
- 类外定义成员函数
1. 格式:返回值类型 类名::函数名(参数){}
2. 说明:类名::
说明后面的函数是类的成员函数,一定要通过对象 / 对象指针 / 对象引用才能调用 - 类成员可被访问的范围
1. private:只有本类内可访问(缺省是这个)
2. public: 任何地方都可以访问
3. protect:本类和子类中可以访问 - 类成员的可访问范围
-
类的成员函数内部,可以访问:
(1). 当前对象的全部属性、函数
(2). 同类其他对象的全部属性、函数 -
类的成员函数以外的地方,可以访问:
(1)该类对象的公有成员 -
解释:设计私有成员的机制,叫
隐藏
,其目的在于强制对成员变量的访问一定要通过成员函数进行,那么以后成员变量的类型等属性修改后,只要更改成员函数即可,否则所有直接访问成员变量的语句都要修改
-
- 成员函数也可以重载或增加缺省参数,规则同普函数,注意避免二义性,比如有重载函数
void setValue();
和void setValue(int val=0);
调用时写setValue();
就会出错
示例程序1
#include<iostream>
#include<string>
using namespace std;
class student
{
private: //私有成员
int age;
string num;
public: //公有成员
void setAge(int age_) //类内定义成员函数
{age=age_;}
int getAge();
void setNum(string num_);
string getNum();
int getStuAge(student s);
};
int student::getAge() //类外定义成员函数
{
return age;
}
void student::setNum(string num_)
{
num=num_;
}
string student::getNum()
{
return num;
}
//类的成员函数可以直接访问作为其参数的同类型对象的私有成员
int student::getStuAge(student s)
{
return s.age;
}
int main()
{
student stu1,stu2;
stu1.setAge(21);
stu1.setNum("011610615");
cout<<"stu1年龄:"<<stu1.getAge()<<endl;
cout<<"stu1学号:"<<stu1.getNum()<<endl;
stu2.setAge(20);
cout<<"stu2年龄:"<<stu1.getStuAge(stu2)<<endl;
}
(2)构造函数初探
-
基本概念:
- 是成员函数的一种
- 名字与类名相同,可以有参数,不能有返回值(void也不行)
- 作用是对对象进行初始化,如给成员函数赋值
- 如果定义类时没显示写出构造函数,编译器自动生成一个默认的无参数构造函数,这个函数不做任何操作
- 对象生成时构造函数自动调用。对象生成后不能再调用了
- 一个类可以有多个构造函数,要求参数个数或参数类型不同(类似重载)
-
为什么需要构造函数:
- 构造函数执行必要的初始化工作,有了构造函数,就不必再专门写初始化函数,也不用忘记担心忘记调用初始化函数
- 有时对象没有初始化就被使用,会导致程序错误
-
构造函数示例:
- 上面那个程序,没人工定义构造函数,是用自动生成的构造函数初始化的
- 下面的程序显示定义了构造函数
-
对象数组的构造函数调用情况
(1) 假设类student有构造函数student()和 student(int n),下面定义对象数组
1. student array[2]; //没指明参数,默认用无参的,两个对象都用 student()构造
2. student array[2]={3,4}; //array[0]用 student(3)构造 ,array[1]用 student(4)构造
3. student array[2]={3}; //array[0]用 student(3)构造 ,array[1]用 student()构造
4. student *array=new student[2]; //两个对象都用 student()构造
(2) 假设类test有构造函数 test(int a); test(int a,int b);和 test(); 三个构造函数
1. test array[3]={1,test(1,2)}; //array[0]用 test(1); array[1]用 test(1,2); array[2]用 test();
2. test *pArray[3] //没有构造函数被调用,因为这里是指针,没有生成实际的对象
3. test *pArray[3]={new test(4),new test(1,2)}; //*pArray[0]用 test(4); *pArray[1]用 test(1,2); pArray[2]指向的对象没有生成,不调用
示例程序2
#include<iostream>
#include<string>
using namespace std;
class student
{
private:
int age;
string num;
public:
student(string num_,int age_=20);
int getAge() {return age;}
string getNum() {return num;}
};
//注意函数缺省值只出现在声明中
student::student(string num_,int age_)
{
num=num_;
age=age_;
}
int main()
{
// student stu; //报错,没有参数
student stu1("123456"); //构造函数使用缺省参数
student *stu3=new student("111111");
student stu2("654321",21); //构造函数不使用缺省参数
cout<<"stu1年龄:"<<stu1.getAge()<<endl;
cout<<"stu1学号:"<<stu1.getNum()<<endl;
cout<<"stu2年龄:"<<stu2.getAge()<<endl;
cout<<"stu2学号:"<<stu2.getNum()<<endl;
cout<<"stu3年龄:"<<stu3->getAge()<<endl;
cout<<"stu3学号:"<<stu3->getNum()<<endl;
delete stu3;
}
(3)拷贝构造函数
- 基本概念:
- 只有一个参数,即同类对象的引用
- 形如 X::X(X &) 或 X::X(const X &),二者选一,后者可以用常量对象作为参数
- 如果没有定义复制构造函数,那么编译器生成默认的复制构造函数,这个函数可以完成两个对象间的复制功能
- 注意:
- 一个类只能有一个复制构造函数
- 拷贝构造函数参数必须是对象的引用
- 复制构造函数起作用的三种情况
- 用一个对象初始化另一个对象,如:
student stu1(stu2);
student stu1 = stu2; - 如果某函数有一个参数为类A的对象,此函数被调用时,类A的复制构造函数被调用,用于初始化局部形参对象
注意:某函数有一个参数为类A的引用,此时没有局部对象被构造,所以不会调用复制构造函数 - 如果函数的返回值是类A的对象,则函数返回时会调用A的复制构造函数生成临时对象,复制构造函数的参数是return的那个对象
注意:对象间的赋值(如stu1=stu2),不调用复制构造函数
- 用一个对象初始化另一个对象,如:
- 常量引用参数的使用
- 上面提到了 :有一个参数为类A的对象的函数,调用时会导致复制构造函数的调用,开销大
要减小开销,可以用类的引用类型作为参数
如果希望确保实参的值在函数中不被改变,可以加上const关键字(修改会报错)
- 上面提到了 :有一个参数为类A的对象的函数,调用时会导致复制构造函数的调用,开销大
示例程序3
#include<iostream>
#include<string>
using namespace std;
class student
{
private:
int age;
string num;
public:
student(string num_="123456",int age_=20);
student(student &stu);
void IncreaseAge() {age++;}
int getAge() {return age;}
string getNum() {return num;}
};
//注意这里不要写缺省值
student::student(string num_,int age_)
{
num=num_;
age=age_;
cout<<"调用构造函数"<<endl;
}
student::student(student &stu)
{
num=stu.num;
age=stu.age;
cout<<"调用拷贝构造函数"<<endl;
}
//观察作为函数参数和返回值时拷贝构造函数的行为
student IncreaseAge(student t)
{
t.IncreaseAge();
cout<<"stu1年龄增加!"<<endl;
return t;
}
int main()
{
student stu1; //构造函数使用缺省参数
student stu2(stu1); //拷贝构造函数
cout<<"stu1年龄:"<<stu1.getAge()<<endl;
cout<<"stu1学号:"<<stu1.getNum()<<endl;
cout<<"stu2年龄:"<<stu2.getAge()<<endl;
cout<<"stu2学号:"<<stu2.getNum()<<endl<<endl;
stu1=IncreaseAge(stu1);
cout<<"stu1年龄:"<<stu1.getAge()<<endl;
}
(4)类型转换构造函数
- 功能:转换构造函数将一个指定的数据转换成类对象
- 只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作转换构造函数 。我们要在类中定义一个只有一个参数的构造函数,参数是待转换类型的数据,在函数体中指定转换的方法
- 当需要的时候,系统会自动调用转换构造函数,建立一个无名的临时对象(临时变量)
- 当出现对象重新赋值时,如下面的c1=9,会先用9作为参数调用类型转换构造函数得到临时对象,然后用此对象给c1赋值
示例程序4
#include<iostream>
#include<string>
using namespace std;
class Complex
{
public:
double real,imag;
Complex(int i)
{
cout<<"调用转换构造函数"<<endl;
real=i,imag=0;
}
Complex(double r,double i)
{
cout<<"调用构造函数"<<endl;
real=r,imag=i;
}
};
int main()
{
Complex c1(7,8);
Complex c2=12; //这种初始化语句,等价于参数为12的构造函数
Complex c3(12); //和上面那条等价
c1=9; //先用9作为参数调用类型转换构造函数得到临时对象,然后用此对象给c1赋值
cout<<c1.real<<","<<c1.imag<<endl;
return 0;
}
三、析构函数
- 什么是析构函数:名字与类名相同,在前面加~,没有参数和返回值,一个类最多有一个析构函数
- 调用时机:对象消亡时自动被调用,可以定义析构函数在对象消亡前做善后工作,比如释放内存空间等(对于非new出来的对象,析构函数不能释放空间)
- 如果没用写析构函数,系统会自己生成一个缺省的析构函数,什么都不做
- 常见的析构函数调用位置
(1). 对象数组生命周期结束时,每一个元素的析构函数都被调用
(2). 对于new出来的对象数组,delete的时候调用析构函数,注意别忘了加[],否则只delete一个对象
(3). 函数形参对象、函数返回的临时对象,在消亡时也会调用析构函数
示例程序5
#include<iostream>
#include<string>
using namespace std;
class String
{
private:
char *p;
public:
String()
{
cout<<"构造函数"<<endl;
p=new char[10];
}
~String();
};
String::~String()
{
cout<<"调用析构函数"<<endl;
delete[]p;
}
String copy(String s)
{
return s;
}
int main()
{
String str[5]; //调用构造函数5次
String *p=new String; //调用构造函数
delete p; //析构
String str1=copy(str[1]); //函数进入时,用拷贝构造函数生成形参对象。离开时形参析构,同时用拷贝构造函数生成临时对象返回,程序结束时这个临时对象析构
cout<<"end main"<<endl;
return 0; //析构5次
}
四、一个例子
- 尝试分析以下程序中构造函数和析构函数的调用情况
示例程序6
#include<iostream>
using namespace std;
class Demo
{
int id;
public:
Demo(int i)
{
id=i;
cout<<"id="<<id<<" 构造"<<endl;
}
~Demo()
{
cout<<"id="<<id<<" 析构"<<endl;
}
};
Demo d1(1); //全局变量,构造
void func()
{
static Demo d2(2); //构造,静态变量离开局部时不会析构
Demo d3(3); //构造,离开局部时会析构
cout<<"func"<<endl;
}
int main()
{
Demo d4(4); //变量初始化,构造
d4=6; //转换构造函数,构造临时对象,给de赋值后临时对象析构
cout<<"main"<<endl;
{
Demo d5(5); //局部变量,进入局部时构造,离开时析构
}
func();
cout<<"main end"<<endl;
return 0; //d4 d2 d1依次析构
}