类是抽象概念,设计类的出发点定义操作,数据存储可以采取多种方式(或者根据操作选择效率最高的数据类型),但是想要施加的操作是唯一的。所以第一步,确定操作,这也是为什么在很多参考书上先写操作,后写数据对象的原因吧。
比如栈吧,把它定义一个类型后,可限制我们对其采用的操作,不然我们完全没必要在定义一个stack类,直接使用vector存储数据不就行了,直接使用vector就是面向过程的编程思想,没有完全封装(限制)任何操作,其实都是定义函数,但是有了类,让函数有了归属感,函数从属于哪个类,哪个类的对象才可以调用。感觉更规整、有规则。
定义了类后,在普通函数的参数、返回类型都可以是我们的类对象,这些函数能调用类对象的接口,利用已经封装好的功能再次扩展功能,这不就类似于我们在函数中使用string对象或是cout,这不过现在使用自定义的类型。
类声明:标清成员的存储权限,若是类声明定义方法,该方法便是内联的;由于我们会将类的头文件包含到多个程序文件中,因此不要在头文件中定义非内联方法(方法只能定义一次),在程序文件定义方法时,需要加类作用域。
#include"stack.h"
#include<algorithm>
bool Stack::push(const std::string& a)
{
if (is_full())
return false;
_elements.push_back(a);
return true;
}
bool Stack::pop()
{
if (is_empty())
return false;
_elements.pop_back();
return true;
}
bool Stack::top( std::string& a)
{
if (is_empty())
return false;
a = _elements.back();
return true;
}
bool Stack::is_full()
{
return _elements.size() == _elements.max_size();
}
bool Stack::is_empty()
{
return _elements.empty();
}
bool Stack::find( std::string & value) {
if (std::find(_elements.begin(), _elements.end(), value) != _elements.end())
return true;
else
return false;
}
int Stack::count(const std::string& value)
{
return std::count(_elements.begin(), _elements.end(), value);
}
#pragma once
#ifndef _STACK_H
#define _STACK_H
#include<vector>
#include<string>
class Stack {
public:
bool push(const std::string&);
bool pop();
bool top( std::string&);
bool is_full();
bool is_empty();
int size() {
return _elements.size();
}
bool find( std::string & value);
int count(const std::string&);
private:
std::vector<std::string> _elements;
};
#endif
#include"stack.h"
#include<iostream>
#include<fstream>
using namespace std;
int main()
{
Stack stack;
string str;
ifstream fin("1.txt");
while (fin >> str && !stack.is_full())
{
stack.push(str);
}
cin >> str;
//cout << str << endl;
cout<<stack.count(str)<<endl;
if (!stack.is_empty())
{
stack.top(str);
cout << str << endl;
}
while (!stack.is_empty())
{
stack.top(str);//top放的位置坑了我
stack.pop();
cout << str << endl;
}
//cin.clear();
cin.get();
cin.get();
return 0;
}
构造函数和析构函数:
对于普通的对象,使用前编译器会检查我们是否为其初始化,那么对于自定义的类对象,编译器又是以何种方式确保我们对其初始化。即根据对象参数调用何时的构造函数。
构造函数创建对象时确保其数据成员有初始值!
类封装了数据成员和方法成员的,同类的不同对象的差别也仅在于数据成员的不同,方法都是一样的。定义类对象和基本类型的对象的思路是一样的,在使用前都得确保有初值!不然数据成员都没有初始值,却在方法中使用这些没有初值的数据,那不乱套了吗?编译器为了防止不发生这样恐怖的事件,确定了构造函数这种机制,迫使创建的类对象的数据成员必须有初始值!所以构造函数设计初衷就是为了调用方法时确保数据成员都是有值的!
编译器到底是如何实现这种机制的呢?
创建对象的时候,会根据所提供的参数个数和类型,匹配合适的构造函数并执行。匹配不到合适的构造函数则编译出错。
接着想,对于基本类型的数据成员,使用赋值运算符就可以解决问题。那么对于类对象的成员,该如何对其进行初始化?显然原先在函数体内简单的赋值语句已经不够用了!这不就有了新的语法规则:成员初值表!在函数体外,类似于调用构造函数为其传递值,为了统一起见,初始化值列表对于基本类型的数据也是适用的!
析构函数:
编译器确保在释放对象前先调用该对象的析构函数。
析构函数应用场景是什么呢?
其实自动变量是会被自动释放的!我们能掌控的无外非是动态变量,这种变量由我们在堆中为其分配空间,并且不会被释放直到我们使用特殊的语句来释放他们。因此,但凡是对象创建了动态空间且没有及时释放,我们的析构函数就很有必要了。
复制拷贝构造函数:
编译器会默认为我们提供这样一个拷贝构造函数,采用逐成员地复制来进行成员初始化。
但是数据成员是指针时,且是指向动态分配的内存空间,这么做会使得一旦有一个对象的生命周期结束调用其析构函数时将连带着将这片动态空间也释放了,会有悬空指针的问题。
如何解决呢?不用默认的拷贝构造函数,自定义拷贝构造函数!采用深度复制,复制内容,不复制地址!
是否需要拷贝构造函数,就看逐成员间的复制是否合适!合适则不用额外定义拷贝构造函数了。
移动构造拷贝函数:
针对复制间效率低下而提出的新语法,C++11前没有这个移动拷贝函数程序照样能用能写,但是有了这个语法的支持,可以提高我们程序运行的效率!这主要是说:参数对象是临时的,这意味什么,这个临时的对象注定要很快就被释放了,那么我们直接使用这个临时对象的存储空间同时更改其数据成员的状态(针对指针,临时对象肯定是要被释放的),而不要再去复制,这里的核心思想是省去复制过程!
唠唠const类对象和cosnt成员函数那点事:
从const普通变量的含义进行推广,定义一个const类对象便是希望这个对象的数据成员值不会被修改,而编译器能检测出任何妄想修改对象数据的操作!
好了,编译器怎么就能知道对象的数据成员有没有被修改呢?数据成员都被声明为私有的,自然若是修改数据成员只能是通过调用方法来实现。为此我们需要使用标识符区别成员方法。这个修饰就是const啦!
为此,编译器会确保const类对象只能调用被修饰为const的成员函数,而且会特别查看const成员方法内是否存在修改类对象的任何数据成员的行为。通过设定关键词和检查特殊方法的定义,感觉检查机制已经很完美了,保证了效率。
通过上述分析可以看出,const对象只能调用const成员方法,但非const对象也可以调用const成员方法!在const方法内决不能存在修改数据成员!
这里有个格外需要注意的地方!一旦方法被声明为const,就需要保证调用对象的数据成员在调用这个方法后数据成员在何种情况下都不能被修改!虽然下面这个例子在方法内没有直接修改数据成员,但是对象在调用这个方法后把数据成员的访问权限释放出去了,这使得调用这个方法后有可能导致使用其他别名来修改这个数据成员的值。这种不确定的情况,必然要扼杀在摇篮里!问题就是编译器如何得知我们不希望返回的数据成员的被修改呢?因此要加以const修饰。那如果就是希望调用后得到这个数据成员并修改这个对象的成员值,就别把这个方法声明为const的。从这里看出不变的是!根据是否修改数据成员来确定方法是否被声明为const,但变的是修改数据成员的行为可以在方法内或是调用方法得到数据成员再来修改!
与普通函数重载相比,多了一个特征标,即是否为const方法!同时包含const方法和非const方法,const对象调用const方法,非const对象调用非const方法!当不包含非const方法时,非cosnt对象调用const方法,有点类似函数参数匹配的过程,优先选择与自己适配程度最高的方法来调用!
但是cosnt对象肯定是不能调用非const方法!这可怎么办呢?有这样一种情况:数据成员并不是抽象出来的类型特征(表征类型状态),可能只是有利用我们操作,并且在进行某种操作后值也会变。const对象能否使得其他数据成员保持不变,而使得这个成员可以变化。(调用非const方法)为此,新的语法便生成了:mutable修饰词!一旦被修饰为mutable了,这个数据成员就不再视为类属性的数据成员,在const方法内自然也可以改变这个值,编译器检测出这种行为后也不会报错。这样一来,const对象便可以调用这样的方法!总之,一旦将成员变量声明为mutable后,即便是在const方法中也是可以改变其值的。
this指针:
想这样一个问题,为什么可以在方法中改变数据成员,这个数据成员是属于哪个类对象的呢?其实编译器在处理成员函数的时候会为其多增加一个参数,这个参数是指向当前调用的对象的指针即this指针(对象存的就是数据值)。因此,可以获取调用对象的数据值!进而才能获取修改该对象的数据成员。一般而言,我们无须显示的使用this指针,但是当需要返回当前调用对象的引用时需要借助this指针!
静态数据成员和静态成员函数:
一个类仅存有一个实体,被同类型多个对象所共享(存取)。可在声明时就初始化。
可以在静态成员方法中访问修改其值,也可以在非静态方法中访问修改其值。但是静态成员函数无法访问使用非静态的数据成员,因此此时数据成员从属于类对象,必须要明确地指出对象的地址。静态方法内部无法调用非静态方法。但非静态方法内部可以调用静态方法。
静态成员函数,编译器在转化静态成员函数的时候不会为其增加调用对象的指针,因此也不能访问到非静态的数据成员。当我们的成员函数与非静态的数据成员无法时,此时可将方法声明为静态的。但在调用的时候,为了和普通的函数区分开来,需要使用类限制符加以修饰,说明这是一个成员函数而不是普通函数,这不以免编译器被搞混了而不知道该调哪个函数。
无论是静态数据成员还是方法,static只在声明时加以修饰!在定义的时候不需要static。
运算符重载函数:
1、可作为成员函数
2、可作为非成员函数
对于以上两种,非成员函数的参数比成员函数的参数多一个!由于成员函数的一个隐形参数是指向调用对象的this指针!
特殊的几个重载函数!
特别地,前置后置自增运算符!为了区别他们,语法规定用一个参数类型是int的特征标来区分他们。此外,对于前置,返回类型可以引用类型,但是对于后置返回的是临时对象!因为要改变成员状态,所以不能再返回引用类型的对象了。
赋值运算符重载函数:
默认的赋值函数执行逐成员的复制,如果逐成员复制无法满足我们的需求,则可以编写一个赋值运算符函数!
既然需要自定义了,想必类的数据成员肯定包含一个指向动态内存空间的指针!需要进行深度复制!
step1:通常先检查对象的地址是否相同
step2:若对象成员包含指针(指向动态空间),需要释放该片内存空间
step3:深度复制
输入输出运算符重载:
只能重载为非成员函数的形式,因为对于两元运算符左侧的操作数为成员函数的隐形this指针,针对cout<<…,我们无法改写ostream类只能使用非成员函数。按照操作数的顺序编写函数。输出对象、输入对象,不就是输出输入对象的数据成员么!返回引用的操作是为了遵守可输入输出流连接的特性!
使用typedef的一些注意事项!
使用typedef的目的是为了简化类型的名称,但是我们在类中使用typedef创建的简化名称在类外不可以直接使用,必须要加上类作用域。
下面这个例子就很好的说明了在类外使用类内设定的别名的用法需要加类作用域!
友元:
可以为一个类声明友元函数、友元类、确切类的友元成员函数(但对于类的成员函数声明为友元函数时,有个向前声明的问题,在为类进行友元方法声明时,需要先定义这个类,使得另一个类得知这个类确实含有这个成员方法),这些类的所有的成员函数或是友元函数可以像这个类的成员函数一般来使用私有成员,是对接口的扩充。
有一个地方之前没太想明白:既然是使用类的私有成员,那肯定地知道对象的实际地址才可能之后获得权限,为此通常来说,成员函数的友元函数或是友元函数极大可能包含一个类对象的参数!进而获取对象的地址再来谈权限!
函数对象:
看见函数调用形式,需要思考这仅仅是个函数名称吗?可以使用这类形式的,除了函数名称,还可以有函数指针、函数对象(定义了运算符()的类,这里区别函数对象的定义和调用的写法(可能都有参数),函数对象的定义前面跟着类型,而函数调用直接使用对象)。
使用函数对象的好处:
主要是使用泛型算法时,作用于元素的函数可能需要两个参数,但一个参数是固定的,之前我们使用了适配器bind1st将函数指针的参数转为1个,但这里提出了新的解决方法,设计函数对象,在类型定义中确定一个成员,并在初始化时就确定这个值,以类的方式解决这个问题。
之前说过函数指针,推广一下成员函数指针该怎么用呢?比如,类的多个成员函数功能相同,在一个方法内能否视情况灵活调用这些个方法呢?函数指针的概念依旧适合类的成员方法!但是存在一些变化!
1、定义成员函数指针需要写清楚类的作用域,每次都要写清楚函数的参数和返回类型,貌似有点麻烦哦,这时可采用typedef简化这一堆的修饰,变为新的类型,再定义变量!
2、为成员函数指针赋值,与普通的函数指针使用方法不同,直接使用函数名就可以了。这里需要对函数名取地址!且写出类作用域!
3、必须使用显示的对象(指向对象的指针)调用指向成员函数的指针。比较令人困惑的地方是在成员函数内部调用成员函数指针时,也得显示写出this指针!!而且由于之前对函数名去过地址,因此在调用的时候需要在指向成员函数的指针前加上*。
附上一个或多或少使用上面方法的小例子:
#include<vector>
#include<iostream>
using namespace std;
class Triangular_iterator {
public:
Triangular_iterator(int next):_ne(next){}
bool operator==(const Triangular_iterator &) const;
bool operator!=(const Triangular_iterator &) const;
int operator*() const;
Triangular_iterator & operator++();
Triangular_iterator operator++(int);
private:
int _ne;
};
class Triangular {
public:
friend ostream & operator<<(ostream &os, Triangular & a);
friend istream & operator >> (istream & is, Triangular &a);
typedef Triangular_iterator iterator;
friend class Triangular_iterator;
int get_len() { return _len; }
int get_begin() { return _begin; }
int get_next() { return _next; }
iterator begin() {
return iterator(_begin);
}
iterator end() {
return iterator(_begin + _len);
}
void get_value(int &) const ;//根据next获取当前元素值
bool get_pos(int pos, int &) const;
static void gen(int n);//根据长度判断是否需要填充static 对象
static bool is_ele(int value);
static void gen_by_val(int value);
Triangular(int l = 0, int b = 0);
void display(ostream &os = std::cout) const;
static void showall();
private:
static vector<int> _eles;//所有对象都共享 每个对象根据数据成员得以区别数列长度 当数列不够长 改变这个数据成员 否则每个对象只是维护各自的标记
int _begin;//透过这些数据成员使得每个对象时有所不同的 第几个元素开始
int _len;//
mutable int _next;//从0开始索引值 初始化为数列的第一个元素 用于遍历 相当于索引
};
class Less_than {
public:
Less_than(int v) { value = v; }
void set_v(int v) { value = v; }
int get_v() {
return value;
}
bool operator()(int v) {
return v < value;
}
private:
int value;
};
#include"triangular.h"
#include<algorithm>
#include<iostream>
#include<vector>
vector<int> Triangular::_eles; //由于静态变量没有在类外再次声明 导致编译器出现无法加载vector!!!
void Triangular::get_value(int & result) const//根据next获取当前元素值
{
result = _eles[_next++];
}
bool Triangular::get_pos(int pos, int &val) const//pos从0开始取值
{
if (pos <= _begin + _len - 1)//begin从0开始取值
{
val = _eles[pos];
return true;
}
return false;
}
void Triangular::gen(int index)// index指出了填充到哪里
{
if (_eles.size() == 0)
{
_eles.push_back(1);
}
int i = _eles.size();
for (; i <= index; i++)
{
_eles.push_back(i*(i + 1) / 2);
}
}
bool Triangular::is_ele(int value)
{
int cur = _eles.back();
if (value > cur)
{
gen_by_val(value);
}
return std::find(_eles.begin(), _eles.end(), value) != _eles.end();
}
void Triangular::gen_by_val(int value)
{
int s = _eles.size();
if (s == 0)
{
_eles.push_back(1);
s++;
}
while (_eles.back() < value)
{
_eles.push_back(s*(s + 1) / 2);
s++;
}
}
void Triangular::display(ostream &os) const
{
for (int i = _begin; i < _begin + _len; i++)
{
os << _eles[i] << " ";
}
os << endl;
}
void Triangular::showall()
{
for (auto x : _eles)
{
std::cout << x << " ";
}
std::cout << std::endl;
}
Triangular::Triangular(int l, int b) {
_begin = (b <= 0 ? 0 : b);
_len = (l <= 0 ? 0 : l);
if (_begin + _len> _eles.size()) {//实际需要的个数和现在向量中的个数比较 若是现有的向量无法满足我们的要求则扩充
gen(_begin + _len - 1);
}
_next = _begin;
//gen(_len);
}
#include"triangular.h"
#include<iostream>
#include<algorithm>
int sum(Triangular & a);
int count_less_value(vector<int> & a, int v);
ostream & operator<<(ostream &os, Triangular & a);
istream & operator >> (istream & is, Triangular &a);
int main()
{
Triangular::gen(10);
cout << "input len and begin:";
Triangular c;
cin >> c;
cout << c;
Triangular b(6, 3);
//b.display();
std::cout << b;
Triangular::showall();
vector<int> test1 = { 1,2,55,3,5,6 };
int num=count_less_value(test1, 10);
cout << "less than 10 " << num << endl;
Triangular a(5,0);
Triangular_iterator it = a.begin();
while (it != a.end())
{
cout << *it << " ";
it++;
}
cout << endl;
int s = sum(a);
cout << s << endl;
// int v;
int m;
cin >> m;
if (Triangular::is_ele(m))
cout << "yes" << endl;
else
cout << "no" << endl;
/*if (a.get_pos(m, v))
cout << v << endl;
else
cout << " out of range!" << endl;*/
Triangular::showall();
a.display();
//Triangular::showall();
//cout << a.get_len() << " " << a.get_begin() << endl;
//b.display();
std::cin.get();
std::cin.get();
return 0;
}
int sum(Triangular & a)
{
int i = a.get_begin();
int len = a.get_len();
int sum = 0;
int temp;
for (; i < a.get_begin() + len; i++)
{
if (a.get_pos(i, temp))
sum += temp;
}
return sum;
}
int count_less_value(vector<int> & a, int v)
{
int count = 0;
Less_than lt(v);
vector<int>:: iterator it=a.begin();
while ((it = std::find_if(it, a.end(), lt)) != a.end())
{
cout << *it << " ";
it++;
count++;
}
cout << endl;
return count;
}
ostream & operator<<(ostream &os, Triangular & a)
{
if (a._begin + a._len > Triangular::_eles.size())
Triangular::gen(a._begin + a._len - 1);
a.display();
return os;
}
istream & operator >> (istream & is, Triangular &a)
{
int beg;
int length;
is >> beg >> length;
a._begin = beg;
a._len = length;
a._next = beg;
if(beg+length>a._eles.size())
Triangular::gen(a._begin + a._len - 1);
return is;
}
枚举:定义一个类型,使得对应该类型的变量取值有限,只能在其枚举值内取值。枚举常量对应着一个int的数,枚举常量可以合适的转化为 int类型,但是int类型必须通过强制类型转化才能变为枚举值(需要在定义的枚举值范围内)并赋值给枚举对象。枚举常量对应的整形数默认从0开始取值,枚举常量们可对应多个相同的整形数。只需要使用枚举值的话,都不需要定义一个类型的标识符,直接可使用这些符号常量。在类外使用符号常量时,记得加类作用域!至于是否需要枚举类型名称根据是否需要枚举类型的变量来决定!
关于枚举和类的使用小例子:
#include<string>
#include<iostream>
#include<cstdlib>
#include<map>
class UserInfo {
public:
enum Level { begin, middle, high };
UserInfo():loginId("guset"),level(begin){//如何使得成员值唯一!利用了局部静态变量!
static int id = 0;
const int size = 16;
char s[size];
_itoa_s(id, s, 10);
loginId += s;
id++;
}
UserInfo(std::string log, Level l) :loginId(log){
level = l;
}
void set_loginId(std::string log)
{
loginId = log;
}
void set_level(std::string lev);
std::string & get_loginId() {
return loginId;
}
std::string get_level();
friend std::ostream & operator<<(std::ostream & os, UserInfo &user);
friend std::istream & operator<<(std::istream & is, UserInfo &user);
private:
static std::map<std::string, Level> levelmap;
std::string loginId;
static void initialmap();
Level level;
};
#include"UserInfo.h"
#include<algorithm>
std::map<std::string, UserInfo::Level> UserInfo::levelmap;
void UserInfo::initialmap()//无法在类外为map初始化!!!
{
levelmap["begin"] =begin;
levelmap["middle"] = middle;
levelmap["high"] = high;
}
void UserInfo::set_level(std::string lev)//string转化到枚举常量 同时利用map做了检查 map用的特别巧 不但检查了键而且存了枚举值 特别好 使用了map解决两种类型问题
{
if (levelmap.empty())
initialmap();
std::map<std::string, Level>::iterator it;
if ((it = levelmap.find(lev)) != levelmap.end())
level = it->second;
else
level = begin;
}
std::string UserInfo::get_level()//枚举常量是整形啊 但我希望返回的是字符串 因此利用枚举常量作为数组的下标即可返回字符串
{
std::string a[] = { "begin","middle","high" };
return a[level];
}
#include"UserInfo.h"
std::ostream & operator<<(std::ostream & os, UserInfo &user)
{
os << user.get_loginId() << " " << user.get_level() << std::endl;
return os;
}
std::istream & operator>>(std::istream & is, UserInfo &user)//所谓的重载没做什么,利用现成的接口拼接即可
{
std::string logid, l;
is >> logid >> l;//这里每次写重载函数的时候总是会被自己写成cin。。。也是醉了。。。。。头脑不清楚
user.set_loginId(logid);
user.set_level(l);
return is;
}
int main()
{
UserInfo u1;
std::cout << u1;
UserInfo u2;
std::cout << u2;
UserInfo u3("hhhh",UserInfo::high);
std::cout << u3;
UserInfo u4;
std::cin >> u4;
//std::cin >> u3;
std::cout << u4;
std::cin.get();
std::cin.get();
return 0;
}