站在编译器和C的角度剖析c++原理, 用代码说话
类模板基础
首先什么是模板: 模板就是把要处理的函数或类的类型参数化,表现为参数的多态性. 模板用来表现逻辑结构相同,但具体数据元素类型不同的对象的通用行为.函数模板我们在上一篇中已经说过了,所以自行回看一下. 我们这里讨论类模板:
类模板用于实现类所需数据的类型参数化.
我们先定义一个模板类:
template<typename T>
class A
{
public:
A(int a)
{
this->a = a;
}
~A()
{
}
T getA()
{
return a;
}
void setA(T a)
{
this->a = a;
}
protected:
private:
T a;
};
这里需要强调一点的是类属参数在类中至少要出现一次. 这样的话,
void printA(A<int> *p)
{
cout<<"打印a:"<<p->getA()<<endl;
}
void printA2(A<char> *p)
{
cout<<"打印a:"<<p->getA()<<endl;
}
void main()
{
A<char> a1;
A<int> b1(19);
b1.setA(10);
cout<<"打印a:"<<b1.getA()<<endl;
return 0;
}
我们再考虑一种情况就是如果存在继承模板类的情况呢?
class C : public A<int>
{
public:
C(int c, int a) :A<int>(a)
{
this->c = c;
}
protected:
private:
int c;
};
void printC(C *myc)
{
cout<<myc->getA()<<endl;
}
int main()
{
C myc(1, 2);
printC(&myc);
return 0;
}
这里注意继承的时候也要public A<int>
写明属性类型. 因为编译器就是根据这个类型来重新构造类的. 经过测试发现一个问题就是在继承中,如果父类没有带参的构造方法的话,在构造子类的时候是不会去父类中的.
类模板派生普通类,在定义派生类时要对基类的抽象类参数实例化. 从普通类派生模板类,意味着派生类添加了抽象类数据成员.
当类模板遇上static
template<typename T>
class A
{
public:
static int m_a;
protected:
private:
T a1;
};
template<typename T>
int A<T>::m_a=0;
int main()
{
A<int> a1;
A<int> a2;
A<int> a3;
A<char> b1;
A<char> b2;
A<char> b3;
a1.m_a ++;
b3.m_a = 199;
cout<<a3.m_a<<endl;//1
cout<<b2.m_a<<endl;//199
return 0;
}
首先我们知道静态成员要在类外面初始化进行激活. 注意使用形式: 首先是template<typename T>
也得指明,然后是涉及到类,就得跟上A<T>
, 最后在写上类型或者返回值就行.
经过程序的执行,我们会发现只要是一种数据类型是共用一个静态区成员变量的,但是不同的数据类型间是独立的. 由此更能看出编译器根据类型进行重新创建类.
当模板类遇上友元函数
在模板类中能够声明各种友元关系:
1. 一个函数或函数模板可以是类或者类模板的成员
2. 一个类或类模板可以是类或类模板的友元类
template<class T>
class Complex
{
public:
Complex(T Real = 0,T Image=0 );
Complex(T a);
friend Complex operator+(Complex &c1, Complex &c2 );//返回匿名对象
void print();
protected:
private:
T Real,Image;
};
template<class T>
Complex<T>::Complex(T Real, T Image)
{
this->Real = Real;
this->Image = Image;
}
template<class T>
Complex<T>::Complex(T a)
{
this->Real = a; this->Image = 0;
}
template<class T>
void Complex<T>::print()
{
cout<<this->Real<<" + "<<this->Image<<endl;
}
template<class T>
complex<T> operator+(Complex<T> &c1, Complex<T> &c2 )
{
Complex<T> tmp(c1.Real+c2.Real, c1.Image + c2.Image);
return tmp;
}
相比我们对友元类友元函数还没陌生吧,如果有些模糊了返回去看看之前的文章吧. 这里简单提一下,友元函数是在类外面定义的函数然后为了使用类中的参数所以要与类构成好朋友的关系.
当我们执行以上函数的时候:
int main0002()
{
Complex<float> c1(1.0, 2.0);
Complex<float> c2(3.0, 4.0);
c1.print();
Complex<float> c3 = c1 + c2;
c3.print();
return 0;
}
是会报错的:
无法解析的外部符号 "
class Complex<float> __cdecl operator+(class Complex<float> &,class Complex<float> &)
" (??H@YA?AV?$Complex@M@@AAV0@0@Z),该符号在函数 _main 中被引用
出现这种错误的原因是当编译器通过类型去构建出一个新的类的时候,没法解析operator+这个符号. 所以这种情况下,我们只能将友元函数的实现放在模板类中了:
friend Complex operator-(Complex &c1, Complex &c2 )
{
Complex tmp(c1.Real-c2.Real, c1.Image - c2.Image);
return tmp;
}
所以一般情况下都是要用到模板类的时候,都是把.h和.c存放在一起去实现的, 只是对友元函数特殊,出现错误就是编译器两次编译按照友元模板的时候出错了.
STL简介
Standard Templete Library是c++的一种标准库,是c++用template机制来表达.
STL使用泛型技术来设计完成的实例,就像MFC是用面向对象技术来设计完成的实例.
STL抽象出这些基本属性成功的将算法和这些数据结构分离,在没有效率损失的情况下,得到了极大的弹性.
那么STL的组成是什么呢?
1. 容器(container)
2. 算法(Algorithm)
3. 迭代器(Iterator)
4. 仿函数(Function Object)
5. 适配器(Adaptor)
6. 空间适配器(allocator)
容器
容器的分类:
1. 序列式容器(sequences containers)
每个元素都有固定的位置,取决于插入时间和地点,与元素值无关.
vector, deque, list
2. 关联式容器(Associated containers)
元素位置取决于特定的排序准则,和插入顺序无关
set, multiset, map, multimap
Vector
vector是一个模板类,在使用模板类的时候要指明具体的类型.
void main01()
{
vector<int> v1(5); //相当于 int v1[5];
//vector<char> v1(5); //相当于 char v1[5];
for (int i=0; i<5; i++)
{
v1[i] = i+1;//重载了[]操作符的
}
for (int i=0; i<5; i++)
{
cout<<v1[i];
}
return 0;
}
我们能发现vector重载了一些运算符,那么我们看看源码中的定义:
reference operator[](size_type n);
const_reference operator[](size_type n) const;
这里值列举单独的一个,可自行查看.
我们来让vector作为函数参数传递:
void printfV(vector<int> &c)
{
int size = c.size();
for (int i=0; i<size; i++)
{
printf("%d ", c[i]);
}
}
int main(void)
{
vector<int> c2(20);
c2 = c1;//重载了等号操作符
printfV(v2);
return 0;
}
这里有人会问vector相当于数组,但是在函数传递的时候不是会退化成指针吗,这样的话不是需要进行数组长度的传递吗?其实不然,因为vector是模板类,是个类,在类中已经是封装了属性方法等的. 将类直接做函数参数传递是可以的.
vector<int>v3(20);
v3.push_back(100);
v3.push_back(101);
printfV(v3);
vector会将20个单元大小全部进行初始化,当我们push_back的时候,会有18个0打出来.
到这里是基本数据类型的用法,我们传个类用用:
struct Teacher
{
char name[10];
int age;
};
void main()
{
Teacher t1, t2, t3;
t1.age = 11;
t2.age = 22;
t3.age = 33;
vector<Teacher> v1(5);
v1[0] = t1;
v1[1] = t2;
v1[2] = t3;
printf("age:%d \n", v1[0].age);
return 0;
}
我们再来看一下传入类指针:
int main()
{
Teacher t1, t2, t3;
t1.age = 11;
t2.age = 22;
t3.age = 33;
vector<Teacher *> v1(5);
v1[0] = &t1;
v1[1] = &t2;
v1[2] = &t3;
for (int i=0; i<v1.size(); i++)
{
Teacher *tmp = v1[i];
if (tmp != NULL)
{
printf("age:%d", tmp->age);
}
}
return 0;
}
是不是感觉很方便,什么类型都能传入,这就是用模板类实现泛型的强大之处.
Iterator
const int arraysize = 10;
int ai[arraysize] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *begin = ai;
int *end = ai + arraysize;
for(int *pi = begin; pi != end; pi++)
{
count << *pi << " ";
}
上面一段的代码就是利用指针来进行数组的遍历,这也就是我们引出的迭代器的本质. 但是当我们使用链表的时候存储空间并不是连续的,所以我们不能通过简单的指针的累加进行查找, 那么就会想到能不能通过一个类似于指针的类对非数组的类进行遍历呢? 我们就能以同样的方式遍历所有的数据结构(容器).
迭代器是一个”可遍历STL容器内全部或部分元素”的对象. 在迭代器中重载了一些操作符:
Operator *
:
传回当前位置上的元素值, 如果该元素拥有成员, 你可以透过迭代器,直接以operator->
取用
Operator++
:
将迭代器前进至下一个元素. 大多数迭代器还可以使用operator--
退回到前一个元素.
Operater==
和Operator !=
:
判断两个迭代器是否指向同一位置.
Operator=
:
为迭代器的赋值(将其指元素的位置指派过去)
有了这个基础知识我们就能介绍list
List
首先我们在c语言中都接触过链表,这也就是是c++为我们封装了些方法让我们去很方便的操作链表. 因为链表不像数组一样那种连续的存储空间,所以当遍历的时候就需要我们用到迭代器.
我们先来简单的用一下list:
void main()
{
list<int> l;
for (int i=0; i<5; i++)
{
l.push_back(i+1);
}
cout<<l.size()<<endl;
list<int>::iterator current = l.begin();//迭代器就相当于是个指针在不断指向
while (current != l.end())
{
cout<<*current<<endl;
current ++;
}
//获取链表的开头,赋值给迭代器
current = l.begin(); //0
current ++; //1
current ++; //2
current ++; //3
l.insert(current, 100);
printf("\n插入位置测试,请你判断是从0位置开始,还是从1位置开始\n");
for (list<int>::iterator p = l.begin(); p!= l.end(); p++)
{
cout<<*p<<" ";
}
return 0;
}
我们能够看到容器中迭代器的使用方式,每个容器模板类中都有自己的iterator方法的. 从这里也能够看出链表就是指针. 然后l.end()是指向了链表的最后一个元素的后一位. 在list中在第三号位置插入就是在第三号前面加入.
我们用类来作为链表类型:
struct Teacher
{
char name[64];
int age;
};
void main()
{
list<Teacher> l;
Teacher t1, t2, t3;
t1.age = 11;
t2.age = 12;
t3.age = 13;
l.push_back(t1);
l.push_back(t2);
l.push_back(t3);
for (list<Teacher>::iterator p = l.begin(); p!=l.end();p++)
{
printf("tmp:%d ", p->age);
}
return 0;
}
相当于是链表中的值是Teacher结构体. 然后迭代器相当于是指针,随意就相当于是指向了每个结构体的指针.
int main()
{
list<Teacher *> l;
Teacher t1, t2, t3;
t1.age = 11;
t2.age = 12;
t3.age = 13;
l.push_back(&t1);
l.push_back(&t2);
l.push_back(&t3);
for (list<Teacher *>::iterator p = l.begin(); p!=l.end();p++)
{
Teacher * tmp = *p;
printf("tmp:%d ", tmp->age);
}
return 0;
}
相当于是将Teacher结构体的指针放入到了list的值域中了. 这样的话迭代器再取地址,就相当于指向指针的指针了,也就是二级指针了.
Stack
栈是先进后出的结构.
void printStack(stack<int> &s)
{
//遍历栈的所有元素,必须要一个一个的弹出元素
while(!s.empty())
{
//获取栈顶元素
int tmp = s.top();
//弹出栈元素
s.pop();
printf("tmp:%d ", tmp);
}
}
void main()
{
//定义了容器类,具体类型int
stack<int> s;//也是模板类
for (int i=0; i<5; i++)
{
s.push(i+1);
}
//遍历栈的所有元素,必须要一个一个的弹出元素
while(!s.empty())
{
//获取栈顶元素
int tmp = s.top();
//弹出栈元素
s.pop();
printf("tmp:%d ", tmp);
}
printf("\n");
printStack(s);
return 0;
}
同样的我们用类作为类型:
struct Teacher
{
char name[100];
int age;
};
void main()
{
stack<Teacher> s;
Teacher t1, t2, t3;
t1.age = 31;
t2.age = 32;
t3.age = 33;
s.push(t1);
s.push(t2);
s.push(t3);
while(!s.empty())
{
Teacher tmp = s.top();
s.pop();
printf("tmp:%d ", tmp.age);
}
printf("\n");
return 0;
}
我们再使用类指针作为参数类型:
int main()
{
stack<Teacher *> s;
Teacher t1, t2, t3;
t1.age = 31;
t2.age = 32;
t3.age = 33;
s.push(&t1);
s.push(&t2);
s.push(&t3);
while(!s.empty())
{
Teacher *tmp = s.top();
s.pop();
printf("tmp:%d ", tmp->age);
}
printf("\n");
return 0;
}
在这里我们将容器用图画出来更形象的比喻一下:
大小端与内存中到底是怎么样存储数组的
在UNP那本书中碰到一个大小端的问题,这里又涉及到栈的问题,所以很想知道栈到底是怎么样分配内存地址的. 我们通常画栈都是开口朝上的,但是其实并不是的.
首先我们能够看出的是栈底是高地址. 并且数组并不是a[0]先入栈的,它的过程是当一旦写int[3]
马上为其分配3个四字节的内存空间在压入栈中,但是当a[0]赋值的时候,会从数组底部开始赋值的. 所以栈中是根据数据类型整体操作的. 内部分配又是另一种方式了. 这个图在搞懂大端小端很有用.
Queue
队列顾名思义就是现进先出的数据结构.
void main()
{
queue<int> q;
for (int i=0; i<5; i++)
{
q.push(i+1);
}
while (!q.empty())
{
//获取队列的第一个元素
int tmp = q.front();
printf("tmp:%d ", tmp);
q.pop();
}
return 0;
}
struct Teacher
{
char name[64];
int age ;
};
void printQ(queue<Teacher *> &myq)
{
while (!myq.empty())
{
Teacher *tmp = myq.front();
if (tmp != NULL)
{
printf("tmp:age : %d ", tmp->age);
}
myq.pop();
}
}
int main()
{
queue<Teacher *> q;
Teacher t1, t2, t3;
t1.age = 32;
t2.age = 33;
t3.age = 34;
q.push(&t1);
q.push(&t2);
q.push(&t3);
printQ(q);
return 0;
}
与stack的思路是一样的.
Algorithm
泛型算法通则:
1. 所有算法的前两个参数都是一对iterators:[first, last), 用来指出容器内的一个范围内的元素.
2. 每个算法的声明中,都表现出它所需要的最低层的iterator类型.
但我们在使用STL中的算法的时候需要引入#include "algorithm"
.
void myPrintFunc(int &v)
{
cout << v;
}
int mycompare(const int &a, const int &b)
{
return a < b;
}
int main()
{
vector<int> v(10);
for (int i = 0; i < v.size(); i++) {
v[i] = rand() % 10;
}
for (int i = 0; i < v.size(); i++) {
printf("%d ", v[i]);
}
for_each(v.begin(), v.end(), myPrintFunc);
sort(v.begin(), v.end(), mycompare);
return 0;
}
STL对数组进行排序的时候,stl实现了排序算法,但是排序的标准可以由用户自己来定义.
GCC
gcc(GNU C Compiler)编译器的作者是Richard Stallman,也是GNU项目的奠基者,那么什么是gcc?
gcc是GNU Compiler Collection的缩写。最初是作为C语言的编译器(GNU C Compiler),现在已经支持多种语言了,如C、C++、Java、Pascal、Ada、COBOL语言等。gcc支持多种硬件平台,甚至对Don Knuth 设计的 MMIX 这类不常见的计算机都提供了完善的支持.
GCC的主要特征:
1. gcc是一个可移植的编译器,支持多种硬件平台
2. gcc不仅仅是个本地编译器,它还能跨平台交叉编译
3. gcc有多种语言前端,用于解析不同的语言。
4. gcc是按模块化设计的,可以加入新语言和新CPU架构的支持
5. gcc是自由软件
GCC的编译过程:
预处理(Pre-Processing)
编译(Compiling)
汇编(Assembling)
链接(Linking)
Gcc *.c -o 1exe (总的编译步骤)
Gcc -E 1.c -o 1.i
Gcc -S 1.i -o 1.s
Gcc -c 1.s -o 1.o
Gcc 1.o -o 1exe
动态库和静态库
1. 一个与共享库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
2. 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该共享库中复制到内存中,这个过程称为动态链接(dynamic linking)
3. 共享库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份共享库被要用到该库的所有进程共用,节省了内存和磁盘空间。
Make
利用 make 工具可以自动完成编译工作。这些工作包括:如果仅修改了某几个源文件,则只重新编译这几个源文件;如果某个头文件被修改了,则重新编译所有包含该头文件的源文件。利用这种自动编译可大大简化开发工作,避免不必要的重新编译。
make 工具通过一个称为 Makefile 的文件来完成并自动维护编译工作。Makefile文件描述了整个工程的编译、连接等规则。
Makefile的基本准则
TARGET … : DEPENDENCIES …
COMMAND
…
目标(TARGET)程序产生的文件,如可执行文件和目标文件;目标也可以是要执行的动作,如clean,也称为伪目标。
依赖(DEPENDENCIES)是用来产生目标的输入文件列表,一个目标通常依赖于多个文件。
命令(COMMAND)是make执行的动作(命令是shell命令或是可在shell下执行的程序)。注意:每个命令行的起始字符必须为TAB字符!
如果DEPENDENCIES中有一个或多个文件更新的话,COMMAND就要执行,这就是Makefile最核心的内容。
Make工具的最核心思想:如果DEPENDENCIES有所更新,需要重新生成TARGET;进而执行COMMAND命令
main:main.o add.o sub.o
gcc main.o add.o sub.o -o main
main.o:main.c add.h sub.h
gcc -c main.c -o main.o
add.o:add.c add.h
gcc -c add.c -o add.o
sub.o:sub.c sub.h
gcc -c sub.c -o sub.o
clean:
rm -f main main.o add.o sub.o
也可以用通配符:
$@ 规则的目标文件名
$< 规则的第一个依赖文件名
$^ 规则的所有依赖文件列表
联系方式: reyren179@gmail.com