C++快速入门学习笔记(四)

三十一、链接和作用域

前边我们已经开始创建由多个文件构成的项目,是时候再来讨论下更复杂的变量作用域了。
简单的理解,变量的作用域就是你可以在什么范围内访问这个变量。
一个在任何函数之前定义的变量可以在任何一个函数里使用(这是一个全局变量),而在某个函数里定义的变量只能在那一个函数里使用(这是一个局部变量)。
那么,当一个项目由多个文件构成时,变量的作用域也会受到一定的影响!

与作用域有关的另一个概念是链接,当你同时编译多个文件时:g++ -o test main.cpp rational.cpp
每个源文件都被称为一个翻译单元(translation unit),在某一个翻译单元里定义的东西在另一个翻译单元里使用正是链接发挥作用的地方。
作用域、链接和存储类是相互关联的概念,它们有许多共同的术语,只是观察和描述问题的角度不同罢了。

存储类(Storage Class)

每个变量都有一个存储类(可以回看c语言笔记中的静态和动态存储方式),存储方式具体包含四种:auto自动,static静态,extern外部,register寄存器,它决定着程序将把变量的值存储在计算机上的什么地方、如何存储,以及变量应该有着怎样的作用域。

  • 默认的存储类是auto(自动),但你不会经常看到这个关键字,因为它是默认的!自动变量存储在称为栈(stack)的临时内存里并有着最小的作用域,当程序执行到语句块或函数末尾的右花括号时,它们将被系统回收(栈回收),不复存在。

  • 与auto不同的是static,static变量在程序的生命期内将一直保有它的值而不会消亡,因为它们是存储在静态存储区,生命周期为从申请到程序退出(和全局变量一样)。一个static变量可以有external或internal链接。

  • 第三种存储类是extern,它在有多个翻译单元时非常重要。这个关键字用来把另一个翻译单元里的某个变量声明为本翻译单元里的一个同名全局变量。注意,编译器不会为extern变量分配内存,因为在其他地方已经为它分配过内存。相当于告诉编译器:这个变量在其他翻译单元里肯定存在,只是没在这个文件里声明而已!

  • 还有一个存储类是register,它要求编译器把一个变量存储在CPU的寄存器里。但有着与自动变量相同的作用域。register变量存储速度最快,但有些编译器可能不允许使用这类变量。

变量的链接和作用域

链接是一个比较深奥的概念,所以我们尽可能以浅显的文字来解释它。在使用编译器建议程序时,它实际上是由3个步骤构成:

  1. 执行预处理器指令;
  2. 把.cpp文件编译成.o文件;
  3. 把.o文件链接成一个可执行文件。

如今的编译器都是一次完成所有的处理,所以你看不到各个步骤(学习win32汇编了解)

  • 步骤一前边我们已经讨论过:执行预处理指令,例如把#include指令替换为相应的头文件里的代码,总的效果是头文件里的代码就像从一开始就在.cpp文件里似的。

  • 步骤二是我们司空见惯的事情:把C++代码转换为一个编译目标文件,在这一步骤里,编译器将为文件里的变量分配必要的内存并进行各种错误检查。

  • 步骤三如果只有一个C++源文件,通常只是增加一些标准库代码和生成一个可执行文件。但当你同时编译多个源文件来生成一个可执行文件的时候,在编译好每一个组件之后,编译器还需要把它们链接在一起才能生成最终的可执行文件。当一个编译好的对象(即翻译单元)引用一个可能不存在于另一个翻译单元里的东西时,潜在的混乱就开始出现了(在链接的过程中会发生检查)

链接分为三种情况,凡是有名字的东西(函数、类、常量、变量、模板、命名空间,等等)必然属于其中之一:外连接(external),内链接(internal)和无链接(none)

外链接的意思是每个翻译单元都可以访问这个东西(前提是只要它知道有这么个东西存在)。普通的函数、变量、模板和命名空间都有外链接。就像main.cpp可以使用rational.cpp文件里定义的类和函数一样,其实我们一直在使用,只是今天我们来一次总结。说到变量,你可以这样试一试:

// this.cpp
int i1 = 1;
// that.cpp
int i2 = i1;

不用试了,一看就有问题,对不对?!在编译that.cpp文件时,编译器并不知道i1变量的存在。
为了解决这个问题,我们可以在that.cpp里使用extern关键字去访问第一个翻译单元的变量。

// this.cpp
int i1 = 1;
// that.cpp
extern int i1;
int i2 = i1;

内链接的含义是:在某个翻译单元里定义的东西只能在翻译单元里使用,在任何函数以外定义的静态变量都有内链接:

// this.cpp
static int d = 8;
// that.cpp
static int d = 9;

这两个文件各有一个同名的变量,但它们是毫不相干的两样东西。

最后,在函数里定义的变量只存在于该函数的内部,根本没有任何链接(none)
但是,如果你能把上面这些关于作用域和链接的事情都搞清楚,就会发现这几个话题其实是彼此紧密相关的。

//header.h
#ifndef HEADER_H
#define HEADER_H

unsigned long returnFactorial(unsigned short num);
//头文件尽量只有声明,不要有定义,为避免重复定义,这里用了const,因为const常量的链接性为内部
static const unsigned short headerNum = 5;

#endif

//that.cpp
#include "header.h"

unsigned short thatNum = 8;
bool printMe = true;

unsigned long returnFactorial(unsigned short num)
{
    unsigned long sum = 1;

    for( int i=1; i <= num; i++ )
    {
        sum *= i;
    }

    if(printMe)
    {
        return sum;
    }
    else
    {
        return 0;
    }
}
//this.cpp
#include "header.h"
#include <iostream>

extern unsigned short thatNum;
static bool printMe = false;//这里必须是static,否则会显示重复定义

int main()
{
    unsigned short thisNum = 10;
    std::cout << thisNum << "! is equal to " << returnFactorial(thisNum) << "\n\n";
    std::cout << thatNum << "! is equal to " << returnFactorial(thatNum) << "\n\n";
    std::cout << headerNum << "! is equal to " << returnFactorial(headerNum) << "\n\n";
    if(printMe)
    {
        std::cout << "因为printMe是static,所以不会被that影响到,这句话不被打印\n";
    }
    return 0;
}

三十二、函数模板

基本的模板语法

到目前为止,我们已经介绍了两种C++程序设计范型,即:

  1. 按照面向过程式范型把程序划分成不同的函数
  2. 按照面向对象式范型把代码和数据组织成各种各样的类并建立类之间的继承关系。
  • 在这一讲介绍另一种范型:泛型编程!泛型编程技术支持程序员创建函数和类的蓝图(即模板,template),而不是具体的函数和类。

  • 这些模板可以没有任何类型:它们可以处理的数据并不仅限于某种特定的数据类型。

  • 当程序需要用到这些函数中的某一个时,编译器将根据模板即时生成一个能够对特定数据类型进行处理的代码版本。

  • 泛型编程技术可以让程序员用一个解决方案解决多个问题。

  • 标准模板库(Standard Template Library, STL)。STL库是泛型编程技术的经典之作,它包含了许多非常有用的数据类型和算法。

  • 在泛型编程技术里,我么仍然需要编写自己的函数和类,但不必限定它们所使用的数据类型。

  • 只需要使用一个占位符(通常用字母T来表示)然后用这个占位符来编写函数。

  • 当程序需要这段代码时,你提供数据类型,编译器将根据你的模板即时生成实用的代码。

  • 简单的说,编译器把模板里的每一个T替换为所提供的数据类型。是不是有点像#define。

以下代码定义了一个名为foo()的函数模板:

template <class T>
void foo(T param)
{
	// do something
}

这里有几件事值得注意:

  • 第一行代码里,在尖括号里有一个class T,用来告诉编译器:字母T将在接下来的函数里代表一种不确定的数据类型。
  • 关键字class并不意味着这个是类,这只是一种约定俗成的写法。
  • 告诉计算机T是一种类型之后,就可以像对待一种普通数据类型那样使用它了。

函数模板

交换两个变量的值是一种几乎所有的程序都需要用到的基本操作。因为这种交换如此常见,所以把它编写为一个函数是个好主意。

void swap(int &a, int &b)
{
    int tmp = a;
    a = b;
    b = tmp;
}
  • 如果我们想用这个函数来交换两个double类型的变量的值,我们应该怎么办?
  • 没错,我们可以再增加一个swap(double &a, double &b)函数,因为C++支持函数重载。
  • 我们发觉不得不为要交换的每一种数据类型反复编写同样的代码。
  • 这正是函数模板大显身手的地方,你用不着为每一种数据类型分别编写一个函数,只要告诉编译器你已经为此准备好了一个模板就行了!
  • 这样子等你再使用swap()函数时,编译器将根据模板自动创建一个函数,该函数会使用正确的数据类型完成交换变量值的任务。
#include <iostream>
#include <string>

template <class T>
void swap(T &a, T &b)
{
    T tmp = a;
    a = b;
    b = tmp;
}
int main()
{
    int i1 = 100;
    int i2 = 200;
    std::cout << "交换前, i1 = " << i1 << ", i2 = " << i2 << "\n";
    swap(i1, i2);
    std::cout << "交换后, i1 = " << i1 << ", i2 = " << i2 << "\n";
    std::string s1 = "小甲鱼";
    std::string s2 = "小黄鱼";
    std::cout << "交换前, s1 = " << s1 << ", s2 = " << s2 << "\n";
    swap(s1, s2);
    std::cout << "交换后, s1 = " << s1 << ", s2 = " << s2 << "\n";
    return 0;
}

注意

  • 创建模板时,还可以用template 来代替template ,它们的含义是一样一样的。

  • template 中的class并不意味着T只能是一个类。

  • 不要把函数模板分成原型和实现两个部分。如果编译器看不到模板的完整代码,它就无法正确地生成代码。所得到的出错信息什么样都有。

  • 为了明确地表明swap()是一个函数模板,还可以使用swap(i1, i2)语法来调用这个函数。这将明确地告诉编译器它应该使用哪一种类型。

  • 如果某个函数对所有数据类型都将进行同样的处理,就应该把它编写为一个模板。

  • 如果某个函数对不同的数据类型将进行不同的处理,就应该对它进行重载。

三十三、类模板

类模板与函数模板非常相似:同样是先由你编写一个类的模板,再由编译器在你第一次使用这个模板时生成实际代码。

template <class T>
class MyClass
{
    MyClass();
    void swap(T &a, T &b);
}

构造器的实现将是下面这样:

MyClass<T>::MyClass()
{
	// 初始化操作。
}

因为MyClass是一个类模板,所以不能只写出MyClass::MyClass(),编译器需要你在这里给出一种与MyClass()配合使用的数据类型,必须在尖括号里提供它。因为没有确定的数据类型可以提供,所以使用一个T作为占位符即可。

编写一个基于模板的栈。栈是实际编程过程中一种非常有用的数据结构,它是一种数据存储机制。
栈只提供两个函数:一个用来把数据压入栈的顶部,另一个用来从栈取出顶部元素(先进后出)

#include <iostream>
#include <string>

template <class T>
class Stack
{
public:
    Stack(unsigned int size = 100);
    ~Stack();
    void push(T value);
    T pop();
private:
    unsigned int size;
    unsigned int sp;
    T *data;
};
template <class T>
Stack<T>::Stack(unsigned int size)
{
    this -> size = size;
    data = new T[size];
    sp = 0;
}
template <class T>
Stack<T>::~Stack()
{
    delete []data;
}
template <class T>
void Stack<T>::push(T value)
{
    data[sp++] = value;
}
template <class T>
T Stack<T>::pop()
{
    return data[--sp];
}
int main()
{
    Stack<int> intStack(100);
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);
    std::cout << intStack.pop() << "\n";
    std::cout << intStack.pop() << "\n";
    std::cout << intStack.pop() << "\n";
    return 0;
}

三十四、内联模板

内联即inline,我们第一想到的是内联函数,引入内联函数的目的是为了解决程序中函数调用的效率问题。
内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。编译时,类似宏替换,使用函数体替换调用处的函数名。
一般在代码中用inline修饰,但能否形成内联函数,需要看编译器对该函数定义的具体处理。

inline int add (int x, int y, int z)
{
	return x+y+z;
}

在程序中,调用其函数时,该函数在编译时被替代,而不像一般函数那样是在运行时被调用。

可以看到,类模板和函数模板的创建过程几乎没有什么区别。
“把相关代码放在一起”这条重要规则同样适用于类模板。
不管是什么模板,编译器都必须看到全部的代码才能为一种给定的类型创建出一个新的实现来。
在创建类模板时,避免类声明和类定义相分离的一个好办法是使用内联方法。
在类里,内联方法的基本含义是在声明该方法的同时还对它进行定义

class Person
{
    Person(std::string name)
    {
        this -> name = name;
    }
	// … …
}

除了可以更好地帮助编译器处理类模板之外,使用内联方法还有一个很好的作用:可以让你少打些字并让源代码的可读性变得更好。

#include <iostream>
#include <string>

template <class T>//类模板
class Stack
{
public:
    Stack(unsigned int size = 100)
    {//内联模板
        this -> size = size;
        data = new T[size];
        sp = 0;
    }
    ~Stack()
    {
        delete []data;
    }
    void push(T value)//函数模板
    {
        data[sp++] = value;
    }
//...............
};
int main()
{
    Stack<int> *s = new Stack<int>(5);
    s->push(5);
    return 0;
}

注意:如果你打算在自己的程序里使用Stack模板,一定要给它增加一个副本构造器和一个赋值操作符重载,就像我们之前做过的例子一样!

我们刚刚的代码还缺少必要的错误处理功能,例如在栈已满的时候调用push()方法,或者在栈为空的时候调用pop()方法,会导致程序运行出错。

我们应该设法让栈在遇到这两种情况的时候抛出一个异常来处理。

C++并没有限制只能使用一个类型占位符,如果类模板需要一种以上的类型,根据具体情况多使用几个占位符即可。

template <class T, class U>
class MyClass
{
	// … …
}

在实例化时,我们只需要这么做:
MyClass<int, float> myClass;

三十五、容器和算法

我们发觉编写的每一个程序都或多或少地需要存储一些数据,而C++在这方面只提供了几种最基本的方法。
你可以创建局部或全局变量来保存单个值,可以使用数组来保存多个值。
今天的概念:能容纳两个或更多个值的数据结构通常我们称为容器(container)。
这么说来,数组是C++唯一直接支持的容器,但数组并不适合用来解决所有的问题。

解决一个问题,找到最合适的容器只是编程工作的一部分。还需要一些适当的函数(算法)来处理这个容器里的数据才能实现最优效率。

向量容器

数组这种数据结构最大的先天不足就是它受限于一个固定的长度。在程序里用int myArray[40]这样的语句定义一个数组时,程序将遇到两个问题:

  1. 首先,你最多只能在那个变量里存储40个整型数据,万一你需要存储第41个数据,那么你就相当不走运了。
  2. 其次,不管程序是不是真的需要存储40个整型数据,编译器都会为它分配40个整型数据的空间。

像这样的问题用C语言解决起来往往很复杂,而C++提供的解决方案就高明得多了。C++标准库提供的向量(vector)类型从根本上解决了数组先天不足的问题。就像可以创建各种不同类型的数组一样,我们也可以创建各种不同类型的向量。std::vector vectorName;

  • 我们用不着对一个向量能容纳多少个元素做出限定,因为向量可以动态地随着你往它里面添加元素而无线增大(前提是有足够可用的内存)
  • 然后你还可以用它的size()方法查知某给定响亮的当前长度(它当前包含的元素个数)
  • 定义一个向量后,我们可以用push_back()方法往它里边添加东西。
  • 我们还可以用访问数组元素的语法来访问某给定向量里的各个元素。
#include <iostream>
#include <vector>
#include <string>
int main()
{
    std::vector<std::string> vec;
    vec.push_back("test");
    vec.push_back("2000");
    for(int i = 0;i<vec.size();i++){
        std::cout << vec[i] <<std::endl;
    }
    return 0;
}
  • C++标准库是用C++编写程序的乐趣之一,它可以让你轻而易举地解决许多非常复杂的问题,我们甚至不必完全了解它的内部工作情况。
  • C++的类型检查功能非常强大,如果你试图把一个其他类型的值放到一个字符串向量里,编译器会立刻报错。
  • 把一些元素放到一个向量里以后,就可以用赋值操作符来改变它们的值了,就像对待数组元素那样:names[0] = “Jonny”;

迭代器

  • 在遍历向量里的各个元素时,我们仍把它视为一个C++数组来对待。刚好我们的向量容器允许使用下表操作符来访问它的各个元素:names[x]。但是如果想改用另一种不提供此方法访问的容器(比如栈),我们就不得不对程序做很多修改才得以实现。
  • 因为对容器里的各个元素进行遍历是一种十分常见的任务,所以应该有一种标准的方式来做这件事,C++标准库提供的各种迭代器(iterator)就是这么来的。迭代器是一种功能非常有限却非常实用的函数,提供一些基本操作符:*、++、==、!=、=。
  • 迭代器是个所谓的智能指针,具有遍历复杂数据结构的能力。
  • 因为迭代器的功能是如此的基本,所以标准库里的每一种容器都支持。通过使用迭代器,当在程序里改用另一种容器的时候就用不着修改那么多的代码了。
  • 每一种容器都必须提供自己的迭代器,事实上每种容器都将其迭代器以嵌套的方式定义于内部。因此各种迭代器的接口相同,型号却不同,这就是所谓泛型程序设计的概念:所有操作行为都使用相同接口,虽然它们的具体实现不同
	std::vector<std::string>::iterator iter = vec.begin();
    while(iter != vec.end()){
        std::cout << *iter <<std::endl;
        iter++;
    }

算法

C++标准库包含着一个经过全面优化的排序算法,它的处理速度也非常理想。

要想使用这个算法,只需先把algorithm文件包含到源文件里:#include

然后再像下面这样调用sort()方法就可以了:std::sort(beginIterator, endIterator);

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
int main()
{ 
    std::vector<std::string> vec;
    vec.push_back("test");
    vec.push_back("ssr");
    vec.push_back("300");
    vec.push_back("2000");
    std::sort(vec.begin(), vec.end());
    std::vector<std::string>::iterator iter = vec.begin();
    while(iter != vec.end()){
        std::cout << *iter  << " ";
        iter++;
    }
    return 0;
}

在这里插入图片描述
最后感谢一下小甲鱼老师。

The end!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值