本文主要内容:
过程性编程和面向对象编程思维,设计类的指导思想:需要什么接口、为了支持接口需要什么数据。
类定义语法:类作用域、访问控制域、const成员函数、数据成员(常量)、创建使用类对象
C++开发者为类提供了一些机制(构造函数析构函数),这使得使用类对象与常规变量很相似。
类对象在内存存储机制,this指针
过程性编程和面向对象编程思维差异:
过程性编程先是考虑步骤,编写合适的函数,再考虑如何表示数据。
面向对象编程先是考虑如何表示数据和考虑如何使用数据(用户接口);再来完成数据存储、如何实现接口。
面向对象编程特性:
抽象、数据隐藏、封装、多态、继承、代码可重用性
抽象和类:
类最吸引人的地方是将数据和方法组合在一起。
用户定义类型是实现了抽象接口的类设计,那么类型是什么?
用多大的内存空间来存储(int 用4个字节来存储),类型可执行的操作(指针无加乘运算)。
设计类的时候,先从接口出发(抽象出最重要的特性),再来确定支持接口所需要的数据。
类定义的两大部分:
1.类声明:描述数据成员和进行成员函数声明(写在头文件),类名称首字母大写
在类声明中可以定义成员函数的,但要求函数体很短,这是因为编译器将其视为内联函数处理。
访问控制:
使用控制符public 、private、protected将成员分为三种控制情况。
public:使用类对象就可以访问到在其中定义的成员
private:使用类对象不可直接访问其中定义的成员,需要调用接口得到、修改私有成员信息,私有成员只能被同类中的成员函数所访问。
封装机制出现在了哪里?
通常将数据成员定义成为私有的,以实现数据隐藏;将实现细节(接口调用实现细节)定义为私有的(便于维护,后续只要修改实现细节而不改动接口);类定义和类声明写在文件中。
类默认访问控制属性为private,因而可省略关键词private。
2.类方法定义:描述如何实现成员函数(写在源代码文件,需要include类声明,否则压根不知道类有哪些数据方法)
与传统的函数定义相比较,加入两个新的特征:
1.定义类成员函数时,函数名前需要指明其所属的类,不然怎么突显这是一个类方法呢?
2.成员函数可使用相同类中的私有成员
方法使用哪些数据呢?
方法所使用的数据是调用方法的对象所存储的数据,每个对象都有一个独立的存储空间存放数据成员,但内存中只有一个方法的副本,并由所有类对象共享。
3.使用类:C++希望用户定义的类和基本类型的使用近似一样,包括声明类变量(自动、静态、动态)、用于函数的实参和返回值,类变量之间可以直接赋值,类变量之间支持自动类型转化,初始化
1.h
//类用于表示股票
#include<string>
class Stock
{
std::string name;//股票名称
int number;//持股数量
double value;//当前价格
double total_v;//是通过另两个数据成员计算而来的
void set_t()//在类声明中定义的函数可视为内联函数
{
total_v = number*value;
}
public:
void initial(std::string ,int ,double);
void buy(int,double);
void sell(int, double);
void show();
};
2.cpp
#include"1.h"
#include<string>
#include<iostream>
void Stock::initial(std::string sname, int count, double p )
{
name = sname;
number = count;
value = p;
set_t();
}
void Stock::buy(int n, double p)
{
number += n;
value = p;
set_t();
}
void Stock::sell(int n, double p)
{
number -= n;
value = p;
set_t();
}
void Stock::show()
{
std::cout << name << " " << total_v << std::endl;
}
3.cpp
#include"1.h"
#include<iostream>
int main()
{
Stock s1;
s1.initial("lin",12,12.2);
s1.show();
s1.buy(88, 100);
s1.show();
s1.sell(33, 20.1);
s1.show();
std::cin.get();
return 0;
}
类的构造函数和析构函数
构造函数:
让我们朝着类的使用能向着基本类型用法靠近来设计类,由于数据成员是私有的,这注定无法像初始化结构那样初始化类对象,必须要调用接口中的函数,构造函数的提出为了解决类对象的初始化。我们希望创建对象的时候能够同时进行初始化,构造函数便能达到这个效果,这是因为在创建对象时将自动调用构造函数来初始化。
控制权限为public
构造函数的参数名不可与数据成员的相同,通常以构造函数的参数为标准来确定数据成员的名称,在名称后加_,或是前加m_
有了构造函数定义后,如何初始化对象呢?
列表初始化:
将{}内的参数与构造函数的形参一一对应起来。
显示调用构造函数:创建对象并为数据成员赋初值。(有些编译器将理解为创建临时对象,在将该对象的数据值复制给新创建的对象)
使用new动态分配对象空间:
隐式调用构造函数:
构造函数不仅能初始化新创建的对象,还能够修改已有对象的数据值,通过对象间赋值操作和初始化都能够改变已有对象的值,但是初始化效率更高。
隐式初始化(编译器调用默认构造函数):
默认构造函数:无参数
没有任何的构造函数定义时,声明类对象,编译器会提供默认构造函数以帮助分配对象所需内存空间完成创建对象。一旦定义了构造函数,编译器不会再提供默认的构造函数,但用户可以重载无参的构造函数或是改变已有的构造函数参数使每个参数都带有默认值。
与编译器提供的默认构造函数不同,自定义的默认构造函数不但能创建对象还能赋初始值。
设计类的时候,一定要有默认构造函数,保证数据成员有初始值。
析构函数:
程序创建对象后,后续将跟踪对象,当对象生命周期结束时,程序将自动调用析构函数完成清理工作,在构造函数内使用new分配内存,则在析构函数中要用delete释放内存,构造函数内没有用new,析构函数内什么都不做。
何时调用析构函数?
静态存储类变量在程序结束后自动调用;自动存储类变量在代码块结束时调用;动态存储类变量在用delete释放类对象存储空间调用。
在引入了构造函数和析构函数,我们赶紧来升级一下我们的程序吧,删去笨重的自定义初始化函数,利用构造函数的概念简化我们的程序吧。
1.h
#include<string>
class Stock
{
std::string name;//股票名称
int number;//持股数量
double value;//当前价格
double total_v;//是通过另两个数据成员计算而来的
void set_t()//在类声明中定义的函数可视为内联函数
{
total_v = number*value;
}
public:
//void initial(std::string ,int ,double);
Stock();//默认构造函数 为了提供给粗心的用户忘记初始化对象数据成员 设计类时就保证每个对象的数据成员有值
Stock(std::string &cname, int ,double);//创建并初始化对象
~Stock();
void buy(int,double);
void sell(int, double);
void show();
};
2.cpp
#include"1.h"
#include<string>
#include<iostream>
Stock::Stock()//默认构造函数 为了提供给粗心的用户忘记初始化对象数据成员 设计类时就保证每个对象的数据成员有值
{
name = "";
value = 0;
number = 0;
total_v = 0;
}
Stock::Stock(std::string &cname, int n, double v)//创建并初始化对象
{
name = cname;
number = n;
value = v;
set_t();
}
Stock::~Stock()
{
std::cout << "destory " << std::endl;
}
void Stock::buy(int n, double p)
{
number += n;
value = p;
set_t();
}
void Stock::sell(int n, double p)
{
number -= n;
value = p;
set_t();
}
void Stock::show()
{
std::cout << name << " " << total_v << std::endl;
}
3.cpp
#include"1.h"
#include<iostream>
int main()
{
{
Stock s1;//默认构造函数
//s1.initial("linsst",12,12.2);
s1.show();
s1.buy(88, 100);
s1.show();
s1.sell(33, 20.1);
s1.show();
Stock s2(std::string("ekek"), 33, 223);//创建对象并初始化
s2.show();
s1 = s2;//类对象之间的赋值,仅仅是数据成员的赋值
s1.show();
s2.show();
s1 = Stock(std::string("ekek"), 222, 1111);//创建了一个临时对象,再将临时对象的值赋值给s1,在释放临时对象空间
s1.show();
Stock s4{ std::string("ekek"),1122,334 };//使用列表初始化对象
s4.show();
std::cin.get();
//std::cin.get();
}
return 0;
}
const成员函数
如果对象是const,编译器要确定对象所调用的函数不会修改对象的数据成员,如果不确定,编译器则会报错。在函数参数列表后加const,即可表明该成员函数不会修改对象的数据成员。只要成员函数不修改对象的数据成员,我们就要把该函数声明为cosnt的,这是个好的编程习惯。
为了在接口中获得数据成员,可以设置一些成员函数,专门用来返回数据成员。由于这些函数简短的特性,因而可以在类声明的时候,直接定义,视为内联函数处理。
this指针:
编译器如何理解对象调用成员函数的呢?凭什么就能获取获取对象的数据成员?
全靠this指针!!!对象在调用成员函数时,传递了对象的地址给成员函数,猜想编译器会为成员函数在编译时多加一个参数,用来存储对象的地址。我们在成员函数所使用的数据成员可以理解为this->数据成员的简写。结合上面的const成员函数,不会修改对象的数据成员,就是因为cosnt修饰的对象的指针,不通过该指针修改对象。当成员函数想要返回传递来的对象时使用*this作为对象的别名。
void Stock::show() const //问题出在 开始时没有声明show为const函数 而p是const引用 确保成员函数不会改变该引用对象的数据成员 所以show必须是const函数
{
std::cout << name << " " << total_v << std::endl;
}
const Stock & Stock:: compare(const Stock & p) const
{
if (p.total_v > total_v)
return p;
else
return *this;
}
const Stock & p=s1.compare(s4);
p.show();
感觉对象引用这块理解不透彻,包括引用理解不到位,还得再看看!!!
对象数组:
对象数组创建及初始化的过程:先是调用默认构造函数创建初始化对象数组的所有元素,如果有显示调用构造函数,则创建临时对象并将数据成员的值赋值给对象数组元素。可以看出创建对象数组的类一定要设计默认构造函数。
下面的例子,显示对象数组初始化,类对象指针如何调用成员函数:
Stock m[SIZE] = { Stock(std::string("ekek"), 222, 1111),Stock(std::string("ekek"),1122,334),Stock (std::string("ekek"), 33, 223) };//对象数组初始化
for (int i = 0; i < SIZE; i++)
m[i].show();
const Stock * temp = &m[0];//类指针暂存最大对象,使用类指针调用函数
for (int i = 1; i < SIZE; i++)
temp=&temp->compare(m[i]);
temp->show();
类作用域:
前面已经学习了局部作用域、全局作用域,有了类,又多了类作用域。
在类中声明的名称作用域整个类;
如何在类外部使用类中标识符?
在之前定义成员函数时,我们已经使用了作用域解析运算符::,或是使用类中方法,使用成员运算符;或是->
常见的场景:
我们希望在类中定义一个常量(比如数组长度),但是类定义只是形式,没有实际分配空间,更不必谈存储常量;为了解决这个问题:提供两种语法:枚举和类静态变量。
枚举:替换,提供一个符号名称,遇到符号常量以数据值来代替。
类静态变量:类静态变量与其余静态变量存在固定内存中,每个对象不单独存储该值而是使用同一个。
抽象数据类型(以通用的方式描述数据类型,而没有引入实现细节):
结合一个具体的实例栈来分析一下用类实现抽象数据类型:实现细节封装在私有部分,接口提供基本操作(入栈、出栈);因而程序员可以随时更改栈的实现而不影响栈使用。
typedef unsigned long Item;
class Stack
{
private:
static const int SIZE = 10;//使用数组存储数据
Item items[SIZE];
int top;
public:
Stack();
bool isempty() const;
bool isfull() const;
bool push(const Item &);
bool pop(Item &);
};
#include"1.h"
#include<iostream>
Stack::Stack()
{
top = 0;
}
bool Stack::isempty() const
{
if (top == 0)
return true;
else
return false;
}
bool Stack::isfull() const
{
if (top == SIZE)
return true;
else
return false;
}
bool Stack::push(const Item &temp)
{
if (isfull())
{
std::cout << "stack if full" << std::endl;
return false;
}
else
{
items[top++] = temp;
return true;
}
}
bool Stack::pop(Item & p)
{
if (isempty())
return false;
else
{
p = items[--top];
return true;
}
}
#include "stdafx.h"
#include"1.h"
#include<iostream>
#include<cctype>
int main()
{
using std::cin;
using std::cout;
using std::endl;
Stack s;
char c;
unsigned long n;
while (cin >> c&&c != 'q')
{
while (cin.get() != '\n')//输入一个字符串,则使用第一个字符删去其余所有字符
continue;
if (!isalpha(c))
continue;
switch (c)
{
case 'A':
case 'a':
cin >> n;
cout << s.push(n);
break;
case 'P':
case 'p':
cout << s.pop(n) << endl;
cout << n << endl;
break;
}
}
return 0;
}