课件内容:
part1 C++模板介绍
part2 泛型编程
part3 容器
part4 一些进阶问题
C++主要有两种类型的模板
1.类模板(Class template):使用泛型参数的类
2.函数模板(Function template):使用泛型参数的函数
模板实例化:显示和隐式两种方式
类模板实参可以是某一型别或常量(仅限int或enum)
C++类模板的声明注意事项:
1.声明类模板和声明函数模板类似
2.关键字class和typename都可以用,但还是倾向于使用typename
templateclass Stack{......};
template class Stack{......};
3.在类模板内部,T可以像其他型别一样(比如int,char等)定义成员变量和成员函数
void Push(const T const& element);
int Pop(T& element);
int Top(T& element) const;
std::vector m_Members;
泛型编程(Generic programming)
1.概观(OverView):泛型编程作为一种编程思维和想法,它是一种编程方法,不依赖于具体的语言。
2.关联特性(Traits)
3.迭代器(Iterators):迭代器是指针的泛化(generalization of pointers)
3.1迭代器本身是一个对象,指向另外一个(可以被迭代的)对象。
3.2用来迭代一组对象,即如果迭代器指向一组对象中的某个元素,则通过increment以后它就可以指向下一组对象中的一个元素。
迭代器(1)
迭代器是指针的泛化(generalization of pointers)
迭代器本身是一个对象,指向另外一个(可以被迭代的)对象
用来迭代一组对象,即如果迭代器指向一组对象中的某个元素,则通过increment以后它就可以指向这组对象的下一个元素
在STL中迭代器是容器与算法之间的接口
算法通常以迭代器作为输入参数
容器只要提供一种方式,可以让迭代器访问容器中的元素即可。
迭代器(2)
迭代器的基本思想
1)在STL中,迭代器最重要的思想就是分离算法和容器,使之不需要相互依赖
2)迭代器将算法和容器粘合(stick)在一起从而使得一种算法的实现可以运用到多种不同的容器上,如下面的例子所示,find算法接收一对迭代器,分别指向容器的开始和终止位置:
templalte
inline _InIt find(_InIt _First,_InIt _Last,const _Ty& _Val){
//find frist matching _Val
for(;_First != _Last;++_First)
if(*_First == _Val)
break;
return (_First);
}
Vector
1.Vector是一个能够存放任意型别的动态数组
2.Vector的数据结构和操作于数组类似,在内存中的表示是一段地址连续的空间。
3.Vector与数组的区别在于,数组大小往往是定义的时候已经指定,而Vector支持动态空间大小调整,随着元素的增加,Vector内部会自动扩充内存空间。
4.为了使用Vector,必须用include指令包含如下文件,并通过std命名空间去访问:
include
int main(){
std::vector v;
}
创建Vector
向Vector中添加元素
判断Vector是否为空
访问Vector里面的元素:
vector::at() //会进行边界检查,判断是否越界
vector::operator[] //不进行边界检查,性能较好
删除元素:
Deque是一个能够存放任意型别的双向队列
Deque提供的函数于Vector类似,新增了两个函数:
push_front:在头部插入一个元素
pop_front:在头部弹出一个元素
Deque采用了于Vector不同内存管理方法:大块分配内存
为了使用deque,必须用include指令包含如下文件,并通过std命名空间去访问:
include
int main(){
std::deque dq;
}
List的优势和劣势
[图片上传中。。。(7)]
常用的操作方法:
作者:易班熊链接:http://www.jianshu.com/p/fa296bf91369來源:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。课件内容:
part1 C++模板介绍
part2 泛型编程
part3 容器
part4 一些进阶问题
C++主要有两种类型的模板
1.类模板(Class template):使用泛型参数的类
2.函数模板(Function template):使用泛型参数的函数
模板实例化:显示和隐式两种方式
类模板实参可以是某一型别或常量(仅限int或enum)
C++类模板的声明注意事项:
1.声明类模板和声明函数模板类似
2.关键字class和typename都可以用,但还是倾向于使用typename
templateclass Stack{......};
template class Stack{......};
3.在类模板内部,T可以像其他型别一样(比如int,char等)定义成员变量和成员函数
void Push(const T const& element);
int Pop(T& element);
int Top(T& element) const;
std::vector m_Members;
泛型编程(Generic programming)
1.概观(OverView):泛型编程作为一种编程思维和想法,它是一种编程方法,不依赖于具体的语言。
2.关联特性(Traits)
3.迭代器(Iterators):迭代器是指针的泛化(generalization of pointers)
3.1迭代器本身是一个对象,指向另外一个(可以被迭代的)对象。
3.2用来迭代一组对象,即如果迭代器指向一组对象中的某个元素,则通过increment以后它就可以指向下一组对象中的一个元素。
迭代器(1)
迭代器是指针的泛化(generalization of pointers)
迭代器本身是一个对象,指向另外一个(可以被迭代的)对象
用来迭代一组对象,即如果迭代器指向一组对象中的某个元素,则通过increment以后它就可以指向这组对象的下一个元素
在STL中迭代器是容器与算法之间的接口
算法通常以迭代器作为输入参数
容器只要提供一种方式,可以让迭代器访问容器中的元素即可。
迭代器(2)
迭代器的基本思想
1)在STL中,迭代器最重要的思想就是分离算法和容器,使之不需要相互依赖
2)迭代器将算法和容器粘合(stick)在一起从而使得一种算法的实现可以运用到多种不同的容器上,如下面的例子所示,find算法接收一对迭代器,分别指向容器的开始和终止位置:
templalte
inline _InIt find(_InIt _First,_InIt _Last,const _Ty& _Val){
//find frist matching _Val
for(;_First != _Last;++_First)
if(*_First == _Val)
break;
return (_First);
}
Vector
1.Vector是一个能够存放任意型别的动态数组
2.Vector的数据结构和操作于数组类似,在内存中的表示是一段地址连续的空间。
3.Vector与数组的区别在于,数组大小往往是定义的时候已经指定,而Vector支持动态空间大小调整,随着元素的增加,Vector内部会自动扩充内存空间。
4.为了使用Vector,必须用include指令包含如下文件,并通过std命名空间去访问:
include
int main(){
std::vector v;
}
创建Vector
向Vector中添加元素
判断Vector是否为空
访问Vector里面的元素:
vector::at() //会进行边界检查,判断是否越界
vector::operator[] //不进行边界检查,性能较好
删除元素:
Deque是一个能够存放任意型别的双向队列
Deque提供的函数于Vector类似,新增了两个函数:
push_front:在头部插入一个元素
pop_front:在头部弹出一个元素
Deque采用了于Vector不同内存管理方法:大块分配内存
为了使用deque,必须用include指令包含如下文件,并通过std命名空间去访问:
include
int main(){
std::deque dq;
}
List的优势和劣势
常用的操作方法:
[图片上传中。。。(8)]
作者:易班熊链接:http://www.jianshu.com/p/fa296bf91369來源:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。课堂大纲
这周的课程大致上应该是这三个部分
- object model部分
- const部分
- new与delete部分
每个部分都讲了一些细节
对象模型(Object Model):
- 对象模型(Object Model):关于vptr和vtbl
在C++中,每个对象实例有不同的vtbl(虚函数表)用图的方式表达如下图;
我对其理解是, 当类中有虚函数的时候,编译器会为类插入一个我们看不见的数据并建立一个表。这个表就是虚函数表(vtbl),那个我们看不见的数据就是指向虚函数表的指针——虚表指针(vptr)。虚函数表就是为了保存类中的虚函数的地址。我们可以把虚函数表理解成一个数组,数组中的每个元素存放的就是类中虚函数的地址。当调用虚函数的时候,程序不是像普通函数那样直接跳到函数的代码处,而是先取出vptr即得到虚函数表的地址,根据这个来到虚函数表里,从这个表里取出该函数的地址,最后调用该函数。所以只要不同类的vptr不同,他对应的vtbl就不同,不同的vtbl装着对应类的虚函数地址,这样虚函数就可以完成它的任务了。
但是,那如果A有两个virtual func B继承并重写了这两个virtual func 而C只重写了一个virtual fuc 那么当调用的时候 c会调用A的另外一个virtual function还是调用B重写的那一个呢?看一下下面的测试代码
#includeusing namespace std;
class A{
public:
int a;
public:
virtual void vfun1(){
cout << " A::vfun1! " << endl;
}
virtual void vfun2(){
cout << " A::vfun2! " << endl;
}
};
class B: public A{
public:
int b;
public:
virtual void vfun1(){
cout << " B::vfun1! " << endl;
}
virtual void vfun2(){
cout << " B::vfun2! " << endl;
}
};
class C:public B{
public:
int c;
public:
virtual void vfun2(){
cout << " C::vfun2! " << endl;
}
};
class D: public C{
public:
int d;
};
int main(int argc, const char * argv[]) {
C* c = new C;
c->vfun1();
D* d = new D;
d->vfun1();
d->vfun2();
return 0;
}
运行结果为:
B::vfun1!
B::vfun1!
C::vfun2!
Program ended with exit code: 0
那结论就是:只要重写virtual function 子类就会继承最新重写的那一个 而非一开始的基类。
- 对象模型(Object Model):关于this
关于this指针,侯老师化的那张图给我们展示了this指针的作用,想起以前在知乎上看过的一个例子形容指针,class类好比一间屋子,类成员比作屋里的东西,而this指针就好比开房子的钥匙,通过他就可以进到屋子,可以拿屋内的各种东西了。
用专业术语来说,this是指向实例化对象本身时候的一个指针,里面存储的是对象本身的地址,通过该地址可以访问内部的成员函数和成员变量。
看一下下面的demo代码:
class Point
{
int x, y;
public:
Point(int a, int b) { x=a; y=b;}
Void MovePoint( int a, int b){ x+=a; y+=b;}
Void print(){ cout<<"x="< };
void main( )
{
Point point1( 10,10);
point1.MovePoint(2,2);
point1.print( );
}
a. 对象point1调用MovePoint(2,2)的时候,即将point1对象的地址传递给了this指针
b. 编译器编译后的原型应该是void MovePoint(Point *this,int a, int b)
c. 在函数体中可以写成{this->x += a; this->y+= b;}
d. 也等价为point1.x += a;point1.y += b。(指针变量通过->访问成员变量(函数),对象通过.)
- 对象模型(Object Model):关于Dynamic Binding
关于C++对象模型中的动态绑定,在看了一些相关资料可认为编译器用静态分析的方法加上虚拟函数的设计实现在程序运行时动态智能执行正确虚拟函数的技术。
在很多资料上都是通过下列demo来理解的
#include
using namespace std;
class A
{
public:
void fA() { cout << "A::fA()" << endl; }
virtual void vfA() { cout << "A::vfA()" << endl; }
void emptyB() { cout << "A::emptyB()" << endl; }
void vfAonly() { cout << "A::vfAonly()" << endl; }
};
class B : public A
{
public:
void fB() { cout << "B::fB()" << endl; }
virtual void vfA() { cout << "B::vfA()" << endl; }
virtual void vfB() { cout << "B::vfB()" << endl; }
void emptyA() { cout << "B::emptyA()" << endl; }
virtual void vfAonly() { cout << "B::vfAonly()" << endl; }
};
int main()
{
A* p = new B;
B& r = *(B*)p;
p->fA(); // 1
//p->fB(); // 2
p->vfA(); // 3
//p->vfB(); // 4
//p->emptyA(); // 5
p->emptyB(); // 6
p->vfAonly(); // 7
cout << endl;
r.fA(); // 8
r.fB(); // 9
r.vfA(); // 10
r.vfB(); // 11
r.emptyA(); // 12
r.emptyB(); // 13
r.vfAonly(); // 14
delete p;
return 0;
}
输出结果如下:
A::fA()
B::vfA()
A::emptyB()
A::vfAonly()
A::fA()
B::fB()
B::vfA()
B::vfB()
B::emptyA()
A::emptyB()
B::vfAonly()
谈谈const
这个在C语言中就是常用的,例如申明常量成员变量函数,想重点总结了一下const在函数中的使用,这部分我以前经常出错。
- const修饰函数参数
a. 传递过来的参数在函数内不可以改变(无意义,因为Var本身就是形参)
void function(const int Var);
b. 参数指针所指内容为常量不可变
void function(const char* Var);
c. 参数指针本身为常量不可变(也无意义,因为char* Var也是形参)
void function(char* const Var);
d. 参数为引用,为了增加效率同时防止修改。修饰引用参数时:
void function(const Class& Var); //引用参数在函数内不可以改变
void function(const TYPE& Var); //引用参数在函数内为常量不可变
这样的一个const引用传递和最普通的函数按值传递的效果是一模一样的,他禁止对引用的对象的一切修改,唯一不同的是按值传递会先建立一个类对象的副本,然后传递过去,而它直接传递地址,所以这种传递比按值传递更有效.另外只有引用的const传递可以传递一个临时对象,因为临时对象都是const属性, 且是不可见的,他短时间存在一个局部域中,所以不能使用指针,只有引用的const传递能够捕捉到这个家伙.
- const 修饰函数返回值
const修饰函数返回值其实用的并不是很多,它的含义和const修饰普通变量以及指针的含义基本相同。
a.
const int fun1() //这个其实无意义,因为参数返回本身就是赋值。
b.
const int * fun2() //调用时 const int *pValue = fun2(); //我们可以把fun2()看作成一个变量,即指针内容不可变。
c.
int* const fun3() //调用时 int *const pValue = fun2(); //我们可以把fun2()看作成一个变量,即指针本身不可变。
一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。
关于new,delete和重载
new和delete也是常用的,新申明的数组需要使用内存时,需要new一段size大小的空间,当使用结束时delete掉,即释放内存,他俩是配合着使用的,不能忘写其中一个。
- new的六种重载形式:
全局的new有六种重载形式,
void *operator new(std::size_t count)
throw(std::bad_alloc); //一般的版本
void *operator new(std::size_t count, //兼容早版本的new
const std::nothrow_t&) throw(); //内存分配失败不会抛出异常
void *operator new(std::size_t count, void *ptr) throw();
//placement版本
void *operator new[](std::size_t count) //
throw(std::bad_alloc);
void *operator new[](std::size_t count, //
const std::nothrow_t&) throw();
void *operator new[](std::size_t count, void *ptr) throw();
所以, 刚才的用法, 就是使用new函数的一种重载形式.
如果A这个对象以同样实行重载了new函数的化, 作为成员函数
会被优先调用.
C++的各种new简介
- new T
第一种new最简单,调用类的(如果重载了的话)或者全局的operator new分配空间,然后用
类型后面列的参数来调用构造函数,用法是
new TypeName(initial_args_list). 如果没有参数,括号一般可以省略.例如
int *p=new int;
int *p=new int(10);
int *p=new foo("hello");
通过调用delete来销毁:
delete p;
- new T[]
这种new用来创建一个动态的对象数组,他会调用对象的operator new[]来分配内存(如果
没有则调用operator new,搜索顺序同上),然后调用对象的默认构造函数初始化每个对象
用法:
new TypeName[num_of_objects];
例如
int *p= new int[10];
销毁时使用
operator delete[]
- new()T 和new() T[]
这是个带参数的new,这种形式的new会调用operator new(size_t,OtherType)来分配内存。
这里的OtherType要和new括号里的参数的类型兼容, 这种语法通常用来在某个特定的地址构件对象,称为placement new,前提是operator new (size_t,void*)已经定义, 通常编译器已经提供了一个实现, 包含头文件即可, 这个实现只是简单的把参数的指定的地址返回,因而new()运算符就会在括号里的地址上创建对象。
需要说明的是,第二个参数不是一定要是void*,可以识别的合法类型,这时候由C++的重载机制来决定调用那个operator new。 当然,我们可以提供自己的operator new(size_,Type),来决定new的行为,比如
char data[1000][sizeof(foo)];
inline void* operator new(size_t ,int n)
{
return data[n];
}
就可以使用这样有趣的语法来创建对象:
foo *p=new(6) foo(); //把对象创建在data的第六个单元上
的确很有意思
标准库还提供了一个nothrow的实现:
void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();
就可以实现调用new失败时不抛出异常
new(nothrow) int(10);
// nothrow 是std::nothrow_t的一个实例
placement new 创建的对象不能直接delete来销毁, 而是要调用对象的析够函数来销毁对象, 至于对象所占的内存如何处理,要看这块内存的具体来源。
- operator new(size_t)
这个的运算符分配参数指定大小的内存并返回首地址,可以为自定义的类重载这个运算符,
方法就是在类里面声明加上
void *operator new(size_t size)
{
//在这里分配内存并返回其地址
}
无论是否声明,类里面重载的各种operator new和operator delete都是具有static属性的
一般不需要直接调用operator new,除非直接分配原始内存(这一点类似于C的malloc)
在冲突的情况下要调用全局的operator加上::作用域运算符:
::operator new(1000); // 分配1000个字节
返回的内存需要回收的话,调用对应的operator delete
- operator new
这个也是分配内存, 只不过是专门针对数组, 也就是new T[]这种形式, 当然, 需要时可以显式调用
- operator new(size_t size, OtherType other_value)
和operator new[](size_t size, OtherType other_value)
参见上面的new()
需要强调的是,new用来创建对象并分配内存,它的行为是不可改变的,可以改变的是各种
operator new,我们就可以通过重载operator new来实现我们的内存分配方案.
题目说明:
为上周题目中的 Fruit和Apple 添加 构造函数与 析构函数,并在构造函数与析构函数中打印控制台信息,观察构造和析构调用过程。然后为Apple类重载::operator new和::operator delete,在控制台打印信息,并观察调用结果。
答案详见:
http://www.jianshu.com/p/18aa33dab0d8