c++学习笔记
标签(空格分隔): 学习笔记
(本学习笔记整理于2016年暑假期间,在编写了超过3000行的代码之后重学c++ primer,获得的一些新鲜想法和技巧)
一、c++学习基础
1.1 c++基础
1).\指出该文件在当前目录中;
2)为了处理输入,使用cin; (cin>>
V1
>>
V2
,第一个输入给
V1
,第二个输入给
V2
)
为了处理处理输出,使用cout;
cerr用来输出警告和错误信息;
clog用来输出程序运行时的一般性信息;
3)<<左侧必须是一个ostream对象;
4)while(cin>>value)语句用来把从键盘的输入传递给value并检查输入是否成功,用来做循环的条件;
5)从键盘输入文字结束符:ctrl + z + enter
6)声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字关联的实体;(extern int val; 它的作用就是告诉编译器这个变量是在其他文件中定义的(是外援),要是在本文件中
看到它的名字千万别奇怪。)
7)在c++新标准下,赋予空指针最好用nullptr;
8)对于const变量,不管是声明还是定义,都添加extern关键词;
9)常量引用需要添加const限定符;
10)为了防止头文件包含重复,可以再头文件中采用以下定义域声明方式:
#ifndef DATA_H
#define DATA_H
...
#endif
11)头文件中不应包含using声明;
12)cin逐个输入,getline读取一整行输入,直到遇到换行符;
1.2字符串、向量和数组
string和vector是两种最重要的标准库类型。string对象是一个可变长的字符序列,vector对象时一组同类型对象的容器。迭代器允许对容器中的对象进行间接访问,对于string对象和vector对象来说,可以通过迭代器访问元素或者元素间的移动。数组和指向数组元素的指针在一个较低层次上实现了与标准库类型string和vector类似的功能。一般来说,应该优先选用标准可提供的实现类型,之后在考虑c++语言内置的底层的替代品数组或指针。
1)string类中,初始化下标为string::size_type,以确保下标的范围不会出现负数。
2)
vector<int>v2;
以上语句用来声明一个空的vector
for(int i =0;i<100;i++)
v2.push_back(i);
以上语句用来向空的vector后方循环插入新值,这也是vector最快的赋值方式。
vector可以用下标引用来直接索引对应位置的值,v2[n];
3)迭代器的初始化: vector::iterator it;
string::iterator it2;
4)访问迭代器所指向的元素需要解引用(*it).empty()或者it->empty.
5)但凡使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
6)强制类型转换:
double slope = static_cast<double>(j)/i;
1.3函数
函数是命了名的计算单元,它对程序个结构化至关重要。每个函数都包含了返回类型、名字、形参列表以及函数实体。函数体是一个块,当函数被调用时候才执行块内的内容。此时,传递给函数的实参类型必须与对应的形参类型相容。在c++语言中,函数可以被重载:同一个名字可用于定义多个函数,只要这些函数的形参数量或者形参类型不同就行。从一组重载函数汇总选取最佳函数的过程称为函数匹配。
1)如果函数无须改变引用形参的值,最好将其声明为常量引用。
bool isShorter(const string &s1, const string &s2);
2)内联函数机制用于优化规模较小,流程直接,频繁调用的函数。
3)任何对类成员的直接访问都被看作this的隐式调用。
1.4 类基础
类是c++语言中最基本的特性。类允许我们为自己的应用定义新类型,从而使得程序更加简洁且易于修改。类有两项基本能力:一是数据抽象,二是封装。类可以定义一种特殊的成员函数:构造函数,其作用是控制初始化对象的方式。构造函数可以重载,构造函数应该使用构造函数初始值列表来初始化所有的数据成员。
1)构造函数,类内初始值:
Sales_data(const std:: string &s):
bookNo(s),Uints_sold(0),reveneu(0){};
2)应该令构造函数初始值的顺序与成员声明的顺序保持一致;
3)静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或者指针来访问静态成员。
4)类似于全局变量,static数据成员一旦被定义,讲讲也一直存在于程序的整个生命周期中。
1.5 标准库基础
c++使用标准库类来处理面向流的输入和输出:iosream处理控制台IO,fstream用来控制命名文件的IO,stringstream完成内存string的IO。每个IO对象都维护一组条件状态,用户来指出此对象上是否可以进行IO操作。
标准库容器室模板类型,用来保存给定类型的对象。所有容器都提供高效的动态内存管理。我们可以向容器中添加元素而不必担心元素存储在哪里。容器负责管理自身的存储。每个容器都定义了构造函数、添加和删除操作,确定容器大小的操作以及返回指向特定元素的迭代器的操作。当我们使用添加或者删除元素的而操作时,必须注意到这些操作可能使指向容器中元素的迭代器、指针或引用失效。
标准库定义了大约100个类型无关的对序列进行操作的算法。算法通过在迭代器上进行操作来实现类型无关。算法从不直接改变他们所操作的序列的大小。他们会将元素一个位置拷贝到另一个位置,但不会直接添加或删除元素
关联容器支持通过关键字高效查找和提取元素。对关键字的使用将关联容器和顺序容器区分开来,顺序容器中是通过位置访问元素的。标准库定义了8跟关联容器(有序无序,关键字是否重复)。有序容器使用比较函数来比较关键字,从而将元素按顺序存储。默认情况下,比较操作时采用关键字类型的<运算符。而无序容器使用关键字类型的==运算符和一个hash<key__type>类型的对象来组织元素。无论在有序容器还是在无序容器中,具有相同关键字的元素都是相邻存储的。
在c++中,内存是通过new表达式来进行分配,通过delete表达式进行释放。新的标准库定义了智能指针类型——shared_ptr和weak_ptr,可令动态内存管理更为安全。对于一块内存,当没有任何用户使用的时候,会自动释放。现代c++程序应该尽可能的使用智能指针。
1)代码通常应该在使用一个流之前检查它是否处于良好状态。
if(out); //out是一个关联命名文件的文件指针
while(cin>>word)
{
}
2)当一个fstream对象被销毁是,close会自动被调用。即使这样,在读写文件操作时,我们也应该显式的调用out.close();
3)c++程序应该使用标准可容器,而不是更原始的数据结构,通常vector是最好的选择,下面列出了容器的选择方案:
a:如果程序要求随机访问元素,应该使用vector或者deque;
b:如果程序要求在容器的中间插入或删除元素,应使用list或forward_list;
c:在头尾位置插入或者删除元素,但不会在中间位置进行插入或者删除从操作,则应该使用deque;
d:如果程序在读取输入时需要在容器中间插入元素,随后需要随机访问容器中的元素。则可以再输入阶段使用list,在操作阶段将list拷贝到vector中。
4)insert函数将元素插入到迭代器所指定的位置之前。
5)
即:c.front() = (c.begin()),c.back = (c.end()-1);
6)容器map,set:允许重复关键字 multi_map(set); 无序的容器 unordered_map(set);
定义map时,必须指明关键字又指明值类型;而定义一个set时,只需指明关键字类型。
map<string,size_t>word_count;
set<string>exclude = {"the","but","and"};
mat<string,string> authors = {{"Jor","Ja"},{"Aus","Ja"},{"Dic","Cha"}};
7)注意Set和multiset的区别,set是具有唯一关键字的容器,multiset允许重复关键字。
8)lower_bound和upper_bound不适用于无序容器,下标和at操作只适用于非const的map和unordered_map。
9)如果一个multimap或者multiset中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。
10)如果author中有search_item,则返回的lower_bound和upper_bound会不等。
for(auto beg = authors.lower_bound(search_item),end = authors.upper_bound(search_item);beg!=end;++beg;)
{
cout<<beg->second<<endl;
}
11)智能指针的陷阱:
a:不适用相同的内置指针初始化多个智能指针;
b:不delete get()返回的指针;
c:不使用get()初始化或reset另一个智能指针;
d:如果使用了get()返回的指针,记住当最后一个对应的智能指针销毁后,你使用的指针会变为无效。
e:如果智能指针管理的资源不是new分配的内存,需要传递一个删除器;
shared_ptr<connection>p(&c,end_connection);
void end_connection(connection *p){disconnect(*p)};
12)new申请的内存进行初始化零值可以使用以下操作
int *array = new int [length]();
1.6类设计者使用工具基础
每个类都会控制该类型的对象拷贝、移动、赋值以及销毁。特殊的成员函数——拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数定义了这些操作。如果一些类没有定义这些操作,编译器会自动为其进行合成。如果这些操作位定义成删除的,他们会逐成员初始化,移动、赋值或销毁对象;合成的操作依次处理每个非static数据成员,根据成员类型确定如何移动、拷贝、赋值或销毁他。
1)如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数式拷贝构造函数。(拷贝构造函数的第一个参数必须是自身类类型的引用类型)
{
sales_Data * p = new sales_Data; //p为动态申请的类指针
auto p2 = make_shared<sales_Data>(); //p2为智能类指针
sales_Data item(*p); //拷贝构造函数将*p拷贝到item中
vector<sales_Data>vec; //局部对象
vec.push_back(*p2); //拷贝P2指向的对象;
delete p;
}
//退出局部作用域,对item,p2,vec调用析构函数;
2)当我们决定一个类是否需要定义它自己的版本的控制成员时,一个基本原则是首先确定这个类是否需要一个析构函数。(需要拷贝操作的类也需要赋值操作)
3)将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本;
class sales_Data
{
public:
sales_Data() = default;
sales_Data(const sales_Data&) = default; //赋值
sales_Data & operator = (const sales_Data&)//拷贝
~sales_Data() = default;
};
sales_Data & sales_Data::operator = (const sales_Data &) = default;
4)拷贝时交换指针优于真实值拷贝
class hasPtr
{
friend void swap(hasPtr&, hasPtr&);
};
void swap(hasPtr& lhs, hasPtr& rhs)
{
using std::swap;
swap(lhs.ps,rhs.ps);
swap(lhs.i,rhs.i);
}
5)message类实例:
class Message
{
friend class Folder;
public:
explicit Message (const std:: string &str = ""):
contents (str){}; //拷贝控制成员,用来管理指向本message的指针;
Message(const Message&); //拷贝构造函数
Message& operator = (const Message&); //拷贝赋值运算符
~Message();
void save(Folder &);
void remove(Folder &);
private:
std::string contents;
std::set<Folder *>folders;
void add_to_Folders(const Message&);
void remove_from_Folders();
};
一个重载的运算符必须是某个类的成员或者至少拥有一个类类型的运算对象。重载运算符的运算对象数量、结合律、优先级与对应的用于内置类型的运算符完全一致。当运算符被定义为类的成员时,类对象的隐式this指针绑定到第一个运算对象。赋值、下标、函数调用和箭头运算符必须作为类的成员使用。
6)重载运算符:重载的运算符是具有特殊名字的函数:他们的名字由关键字operatpr和其后要定义的运算符号共同组成。
data1 + data2; 等价于 operator+(data1,data2);
data1 += data2; 等价于 data1.operator +=(data2);
7)某些运算符不应该被重载:逻辑与,逻辑或,逗号运算符(&&,||,“,”,&);
8)赋值(=),下标([]),调用(())和成员访问箭头(->)运算符必须是成员函数;递增,递减和解引用运算符通常应该是成员函数;算术(+,-,*,/,%)相等性、关系和位运算符,通常应该是非成员函数。
9)重载输出<<的例子:
ostream &operator<<(ostream &os, const sales_Data &item)
{
os<< item.isbn()<<""<<item.uints_sold<<""
<<item.revenue<<""<<item.arg_price();
return os;
}
//尽量减少格式化操作,尽量减少换行符、制表符
重载输入>>运算符
istream &operator>>(istream &is, sales_Data & item)
{
double price;
is>>item.bookNo>>item.uint_sold>>price;
if(is)
item.revenue = item.uint_sold * price;
else
item = sale_Data();
return is;
}
输入输出运算符必须是非成员函数,一般被声明为友元函数;
10)重载下标运算符
class strVec
{
public:
std::string & operator[](std::size_t n)
{return elements[n];}
const std::string & operator[](std::size_t n)const
{return elements[n];}
private:
std::string * elements;
};
//下标运算符必须是成员函数
11)从一个类转换到另一个类
operator type()const;
oop面向对象的编程的核心思想是:数据抽象,继承和动态绑定。继承使得我们可以编写一些新的泪,这些新类既能共享其基类的行为,又能根据需要覆盖或添加行为。动态绑定使得我们可以忽略类型之间的差异。在c++语言中,动态绑定值作用于悬殊,并且需要通过指针或引用调用。在派生类对象中包含有与它的每个基类对应的子对象,因为所有派生类对象都含有基类部分,所以我们能将派生类的引用或指针转换为一个可访问的基类引用或指针。当执行派生类的构造、拷贝、移动和赋值操作的时候,首先构造、拷贝、移动和赋值其中的基类部分,然后才轮到派生类部分。而析构函数的执行顺序则正好相反。基类通常都应该定义一个虚析构函数。
12)面向对象的编程的核心思想:数据抽象、继承、动态绑定。数据抽象使得接口与实现的分离;继承可以定义相似类及其建模;动态绑定在一定程度上忽略了相似类型的区别。
13)层次关系的根部有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类称为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
14)protected成员:供派生类访问,禁止其他用户访问。
15)派生类也必须使用基类的构造函数来初始化它的基类部分
Bulk_Quote(const satd::string& book, double p,std::size_t qty, double disc):
Quote(book,p),min_qty(qty),discount(disc){};
16)派生类的声明:与其他类相同,不包含公共派生列表。如果想将某类用作基类,则该类必须已经经过声明和定义。
17)基类应该将其接口成员声明为公有的;同时将属于其实现的部分分成两组。一组可供派生类访问,另一组只能由基类及基类的友元函数访问。
18)除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。
19)派生类继承基类的构造函数:提供一条注明了基类名的using声明语句。
20)函数模板
template<typename T>
int compare(const T& v1, const T& v2)
{
if(v1 < v2) return -1;
if(v2 < v1) return 1;
return 0;
}
21)函数模板的inline声明:
template <typename T> inline T min(const T &, const T &);
二、类的设计者编程实例
基类以及派生类的声明
//dma.h --类的继承关系以及动态内存在基类和派生类中的使用
#ifndef DMA_H_
#define DMA_H_
#include <iostream>
//Bass class using dma
class baseDMA
{
private:
char* label;
int rating;
public:
baseDMA(const char* l = "null", int r = 0); //构造函数
baseDMA(const baseDMA &rs); //赋值函数
virtual ~baseDMA(); //基类的虚析构函数
baseDMA &operator = (const baseDMA & rs); //移动拷贝函数
friend std::ostream &operator <<(std::ostream & os, const baseDMA &rs); //重载的输出操作符
};
//derived class without DMA
//no destructor needed
//uses implicit copy constructor
//uses implicit saaignment operator
class lacksDMA : public baseDMA
{
private:
enum {COL_LEN =40};
char color[COL_LEN];
public:
lacksDMA(const char* c = "blank", const char * l = "null", int r = 0);//派生类的构造函数,想给派生类的变量进行初始化,在对基类的变量进行初始化,顺序保持一致
lacksDMA(const char* c, const baseDMA& rs);//派生类的赋值函数
friend std::ostream &operator<<(std::ostream &os,const lacksDMA &rs);
};
//derived class with DMA
class hasDMA :public baseDMA
{
private:
char* style;
public:
hasDMA(const char* s = "none", const char* l = "null", int r = 0);
hasDMA(const char * s, const baseDMA& rs);
hasDMA(const hasDMA &hs);
~hasDMA();
hasDMA &operator = (const hasDMA & rs); //拷贝赋值函数
friend std::ostream & operator<<(std::ostream &os, const hasDMA& rs);
};
#endif
基类以及派生类的方法定义:
//dma.cpp --dma class methods
#include "dma.h"
#include <string>
using namespace std;
//baseDMA methods
baseDMA::baseDMA(const char * l, int r)
{
label = new char[strlen(l) + 1];
strcpy(label, l);
rating = r;
}
baseDMA::baseDMA(const baseDMA &rs)
{
label = new char[strlen(rs.label) + 1];
strcpy(label, rs.label);
rating = rs.rating;
}
baseDMA::~baseDMA()
{
delete[] label;
}
baseDMA & baseDMA::operator=(const baseDMA & rs)
{
if (this == &rs)
{
return *this;
}
delete[] label;
label = new char[strlen(rs.label) + 1];
strcpy(label, rs.label);
rating = rs.rating; return *this;
}
ostream &operator <<(ostream &os, const baseDMA & rs)
{
os << "Label:" << rs.label << endl;
os << "Rating:" << rs.rating << endl;
return os;
}
//lacksDMA methods
lacksDMA::lacksDMA(const char * c, const char* l, int r)
:baseDMA(l, r)
{
strncpy(color, c, 39);
color[39] = '\0';
}
lacksDMA::lacksDMA(const char* c, const baseDMA &rs)
:baseDMA(rs)
{
strncpy(color, c, COL_LEN - 1);
color[COL_LEN - 1] = '\0';
}
ostream &operator <<(ostream & os, const lacksDMA& ls)
{
os << ls;
os << "Color:" << ls.color << endl;
return os;
}
//hasDMA methods
hasDMA::hasDMA(const char* s, const char* l, int r)
:baseDMA(l,r)
{
style = new char[strlen(s) + 1];
strcpy(style, s);
}
hasDMA::hasDMA(const char* s, const baseDMA & rs)
:baseDMA(rs)
{
style = new char[ strlen(s) + 1];
strcpy(style, s);
}
hasDMA::hasDMA(const hasDMA & hs)
:baseDMA(hs)
{
style = new char[strlen(hs.style) + 1];
strcpy(style, hs.style);
}
hasDMA::~hasDMA()
{
delete[] style;
}
//移动拷贝构造函数
//即相当于重载=运算符来完成拷贝构造函数的功能,等价于hasDMA(const hasDMA & hs)
hasDMA &hasDMA:: operator=(const hasDMA &hs)
{
if (this == &hs)
{
return *this;
}
baseDMA::operator = (hs); //首先调用基类的拷贝函数
style = new char[strlen(hs.style) + 1];
strcpy(style, hs.style);
return *this;
}
ostream & operator<<(ostream & os, const hasDMA &hs)
{
os << hs;
os << "Style:" << hs.style << endl;
return os;
}