Marco Nie - 2020年11月 you are the company you keep... 2020-11-25T11:52:00+08:00 Typecho https://blog.niekun.net/feed/atom/2020/11/ <![CDATA[c++ 中 string 字符串的含义]]> https://blog.niekun.net/archives/1953.html 2020-11-25T11:52:00+08:00 2020-11-25T11:52:00+08:00 admin https://niekun.net 有如下代码:

int main()
{
    char *s;
    s = "hello";

    cout << *s << endl;
    cout << s << endl;

    return 0;
}

//OUTPUT:
//h
//hello

首先定义了一个指针,然后给指针赋值为一个字符串。看起来以上写法有错误,s 指针是一个地址,为什么给它赋值一个字符串?

因为在 c++ 中字符串类似于我们提到的数组,使用数组名就表示第一个数组元素的地址。例如:

int main()
{
    int arr[] = {1, 3};
    int *p = arr;

    cout << *p << endl;
    cout << *(p+1) << endl;

    return 0;
}

//OUTPUT:
//1
//3

同样在字符串中,s = "hello" 表示将首字母 h 的地址赋给指针。

]]>
<![CDATA[c++ 中数组类型数据作为 function 参数时的注意事项]]> https://blog.niekun.net/archives/1946.html 2020-11-25T11:16:00+08:00 2020-11-25T11:16:00+08:00 admin https://niekun.net 关于 function 的基础教程参考:C++ 入门教程之四 -- Functions

当 function 的传入参数为数组时,如果我们想要得到数组元素个数可能会这样写:

void testFunc(int arr[]) {
    int count = sizeof(arr)/sizeof(arr[0]);
}

int main()
{
    int arr[] = {1, 3, 6, 8};
    cout << testFunc(arr) << endl;

    return 0;
}

sizeof function 可以返回数据占用内存空间的大小。所以用整个 arr 大小除以第一个元素大小可以得到数组元素个数。

但是测试会发现以上得到的 count 值并不是 arr 数组元素个数 4 而是 2。这是因为当 function 传入数组数据时,会自动将其转换为 pointer 指针类型。这时候使用 sizeof() 得到的不是 arr 数据所在地址的内存大小,而是这个指向 arr 数组的指针所占用内存大小。

我们做个实验:

int main()
{
    int *arrP = new int[4];
    cout << sizeof(arrP) << endl;
    cout << sizeof(arrP[0]) << endl;
    return 0;
}

//output:
//8
//4

以上建立了一个指针指向 4 个元素的数组,然后分别查看其整个指针和某个元素指针的内存大小,得到的结果是 8 和 4。所以他们两个相除结果为 2。

因为指针指向的是数组第一个元素,如果我们要通过指针给数组元素赋值,可以使用 *(pointer + n) 的方式完成,例如:

int *arrP = new int[4];
*arrP = 1;
*(arrP+1) = 2;

*(arrP+1) 就是第 2 个数组元素。

可以用指针查询数组中某个元素的内存大小,数组名 arr 表示数组所在地址(不需要加地址转换符&):

int main()
{
    int arr[] = {1, 3, 6, 8};
    int *arrP = arr;

    cout << sizeof(*arrP) << endl;
    cout << sizeof(*(arrP+1)) << endl;

    return 0;
}

//output:
//4
//4

由于 arr 数组名表示此数组第一个元素所在地址的指针,所以我们可以直接使用此名称来用指针的方式地区元素数据:

int main()
{
    int arr[] = {1, 3, 6, 8};
    cout << *arr << endl;
    cout << *(arr+1) << endl;

    return 0;
}

//output:
//1
//3

那么如何在 function 使用数组作为传入数据时如何能够直接使用它的数据而不是指针类型呢?可以使用以下方法:

template <size_t N>
int testFunc(int (&arr)[N]) {
    int count = sizeof(arr) / sizeof(arr[0]);
    return count;
}

int main()
{
    int arr[] = {1, 3, 6, 8};
    int count = testFunc(arr);

    cout << count << endl;
    return 0;
}

//OUTPUT:
//4

创建一个 function template,size_t 类型表示任意数据类型的元素占用内存大小,此处将其作为 generic data type 用来定义实际传入的数组所占用内存容量的 template,然后以传入数据的实际地址的模式将数组的元素都顺序都传入 function。这样传入数据就是实际数据地址而不是指针。

关于 function 的传入数据实际地址是使用指针形式 * 还是 & 形式,在前面我介绍过,形式如下:

void test1(int *a) {
    *a = 3;
}

void test2(int &a) {
    a = 2;
}

int main()
{
    int a = 1;
    int *p = &a;

    test1(p);
    cout << a << endl;

    test2(a);
    cout << a << endl;

    return 0;
}

//OUTPUT:
//3
//2

function 中使用在指针类型的传入数据时需要传入一个指针,当定义为 & 时可以直接传入数据名称。两种方法都是 by reference 会影响到对应内存地址的数据内容。可参考教程:C++ 入门教程之四 -- Functions

参考链接:
https://stackoverflow.com/questions/4839626/element-count-of-an-array-in-c
https://www.tutorialspoint.com/cplusplus/cpp_pointer_to_an_array.htm

]]>
<![CDATA[C++ 入门教程之八 -- Templates - Exceptions - Files]]> https://blog.niekun.net/archives/1940.html 2020-11-23T21:41:00+08:00 2020-11-23T21:41:00+08:00 admin https://niekun.net function template

使用 function 和 class 使我们编程更加简单及易操作。但是它们仍然受限于 c++ 编程规范,在定义它们的参数时必须指定参数的类型。例如:

int sum(int a, int b) {
  return a+b;
}

int main () {
  int x=7, y=15;
  cout << sum(x, y) << endl;
}

以上示例中,需要我们定义sum function 需要传入两个 int 型数据,返回其相加结果。在 main 中调用 sum function。

sum() 可以正确的执行指令,但它的限制是必须传入 int 类型的数据。

如果需要实现两个 double 类型数据相加,需要再次定义新 funtion:

double sum(double a, double b) {
  return a+b;
}

使用 function template 能够让我们只定义一个 sum() 来适用于所有类型的数据。

使用关键词 template 来定义一个 function template,在尖括号<>内定义通用数据类型:

template <class T> 

关键词 class 表示一个申明要定义 generic 通用数据类型,注意不要和我之前学习的 class 混淆。

也可以使用关键词 typename

template <typename T> 

T 表示我们的 generic 通用数据类型名称,在后续中可以使用此名称代表数据类型,可以是任意自定义字符

修改我们的示例:

#include <iostream>
using namespace std;

template <class T>
T sum(T a, T b)
{
    return a + b;
}

int main()
{
    int x=7, y=15;
    cout << sum(x, y) << endl;

    double a=7.15, b=15.54;
    cout << sum(a, b) << endl;

    return 0;
}

我们在 function 中建立了一个 generic data type 通用数据类型 T,返回值和传入参数类型都为 T。在 mian 中调用时,会根据实际传入数据类型来动态确定 T 的实际类型。

function template 可以节省编程中很多时间,因为只需要一次定义就可以兼容不同的数据类型。

我们也可以同时定义多个 generic data type,使用逗号, 分隔多个定义类型:

template <class T, class U>
T smaller(T a, U b) {
  return (a < b ? a : b);
}

int main () {
  int x=72;
  double y=15.34;
  cout << smaller(x, y) << endl;
}

以上示例定义了两个通用数据类型,返回值类型为第一种数据类型。(a < b ? a : b) 表达式的意思是:判断 a 是否小于 b,如果结果为 true 则返回 a 的值,如果结果为 flase 返回 b 的值,两个参数可以是不同数据类型,如:int 和 double。

在 mian 中调用 smaller function,返回结果为两个参数中较大的那个的值,由于返回值类型为第一个定义数据类型 T,而我们调用时传入的第一个参数类型为 int,所以返回结果也为 int 型。

需要注意的是在一旦定义了 function template,在 function 中就一定要使用定义的那些通用数据类型,否则编译器会报错。

class template

同样也可以定义 class template,允许 class 的元素类型为 generic data type。语法如下:

template <class T>
class MyClass {

};

类似于 function template,多个 generic data type 使用逗号, 分隔:

template <class T,class U>

下面举例说明使用方法:

template <class T>
class Pair {
    public:
    Pair (T a, T b):
    first(a), second(b) {
    }
    
    private:
    T first, second;
};

以上建立了一个 class template,类型为 T,通过构造器对 private 里的两个 T 类型的参数初始化数据。

在 class template 外定义 function,如 class 大括号外或一个单独文件,需要在 class 后标记 class template 内定义过的 generic type。例如在以上示例的 class 外建立 bigger function:

template <class T>
class Pair {
    public:
    Pair (T a, T b):
    first(a), second(b) {
    }
    T bigger();

    private:
    T first, second;
};

template <class T>
T Pair<T>::bigger() {
    return (first>second ? first : second);
}

上面示例中使用了 scope resolution operator - 范围解析符:: 来表示外部的 Pair() function 属于哪个 class,在头文件章节介绍过,具体参考:C++ 入门教程之六 -- Classes 实践

bigger function 返回 class 两个属性变量中较大的值。

在 main 中实例化 Pair class 时需要使用尖括号定义当前实际要使用的数据类型,如 int 类型:

void main() {
    Pair<int> obj(11, 22);
    cout << obj.bigger() << endl;
}

也可以定义 double 类型:

void main() {
    Pair<double> obj(11.23, 22.56);
    cout << obj.bigger() << endl;
}

template 特例

在前面介绍的 class template 中,实例化 object 时定义不同的数据类型会执行同样的 function 指令内容。template specialization 特例允许当我们定义某些特定数据类型作为 template type 时执行和通用指令不同的内容。

下面举例说明,当定义数据类型为 char 时执行和其他数据类型不同的指令,先建立一个常规 class template:

template <class T>
class MyClass {
public:
    MyClass(T x) {
        cout << x << " - not a char" << endl;
    }
};

以上 class 用来作为常规数据类型情况下的处理。

为了处理当数据类型为 char 时的情况,我们建立一个 class specialization 特例:

template <>
class MyClass<char> {
public:
    MyClass(char x) {
        cout << x << " - is a char" << endl;
    }
};

以上代码中,首先在 class 前声明了一个没有参数的 template <>,这是为了区别于通用数据类型情况,表明此 class specialization 里的数据类型是已知的和特定的。而由于这个 class 依然是属于 class template 类型的,只是一个特例情况的处理,所以这句声明不能省略。

在 class template 名称后面的 <char> 定义了此 clas 属于对哪种 specialization 特例数据类型的处理。当我们实例化时,如果定义的数据类型是属于 template specialization 里定义的类型时,会将此 class 作为实例化对象。

需要注意得是对常规数据类型的 class template 和特定类型数据的 class template 两者的 body 内容是完全独立互不影响的,specialization template 并不从 generic template 里继承任何元素,如果需要的话可以编写完全不同的功能。

在 main 中使用不同数据类型进行实例化测试:

int main() {
    MyClass<int> obj1(22);
    MyClass<double> obj2(11.34);
    MyClass<char> obj3('a');
    
    return 0;
}

//output:
//22 - not a char
//11.34 - not a char
//a - is a char

可以看到 generic template 适用于数据类型:int 和 double,而 specialization template 适用于数据类型:char。

exception 例外

程序在执行中遇到问题叫做:exception 例外。

在 c++ 中,exception 是对程序遇到反常情况的反应。如:0做除数时。

c++ 的 exception handling 例外管理器是通过三个关键词:trycatchthrow 来建立的。throw 是用来当问题出现时调出某一个 exception 响应动作的。例如:

int motherAge = 40;
int sonAge = 50;
if (sonAge > motherAge) {
    throw 99;
}

以上示例中,当 sonAge 大于 motherAge 时,程序首先会自动识别 throw 语句中元素的数据类型,然后根据这个数据类型去寻找处理这类数据类型的 exception 块。最后使用 throw 内的数据作为传入参数在 exception 中使用。

throw 中的数据类型可以是任意的,如 123 为 int 型,a 为 char 型,11.24 为 double 型,一下示例写法都是正确的:

int motherAge = 40;
int sonAge = 50;
if (sonAge > motherAge) {
    throw ‘a’;
}

if (sonAge = motherAge) {
    throw 12.45;
}

catching exception

那么具体如何响应 exception 呢?需要使用 try/catch block 模块来构造完整的 throw 和 response 过程。一个 try block 块用来激活特殊情况的 exception 功能,在 try 后面需要跟一个或多个 catch block 块来执行某种特定 throw 类型的 exception 动作。在 catch 后需要定义特定的数据类型对应于 throw 的某种数据类型。

下面举例说明:

try {
    int motherAge = 40;
    int sonAge = 50;
    if (sonAge > motherAge) {
        throw 99;
    }
} catch (int x) {
    cout << "wrong age value - Error" << x << endl;
} catch (char x) {
    cout << "example for other catch" << x << endl;
}

//output: wrong age value - Error99

以上例子中,try 块 throw 了一个 exception,throw 的数据 99 是 int 类型,所以匹配到 catch 中定义类型也为 int 型的 exception 块。然后将 99 作为传入数据在 exception 中使用。当只有一种 exception 情况是,只需要定义一个 throw 和一个 catch 块即可。

下面的示例是提示用户输入两个数字,然后将它们相除,exception 的情况是当第二个数为 0:

try {
    int num1;
    cout << "enter the first number:";
    cin >> num1;
    
    int num2;
    cout << "enter the second number:";
    cin >> num2;
    
    if (num2 == 0) {
        throw 0;
    }
    cout << "result: " << num1 / num2 << endl;
} catch (int x) {
    cout << "division by zero!" << endl;
}

cin 是 istream class 类型的 object 用来输入数据流,数据输入后会存在后面的变量中。当输入的第二个数字非零时不会触发 exception 响应,当为零时 throw 类型为 int,然后匹配到 exception 中类型也为 int 的块,然后执行其中指令。

如果以上程序中不考虑输入数据是否为 0,则当除数为 0 时程序会崩溃。

catch 块也可以管理在 try 块中任何类型的 throw exception,不同于catch 后定义某一种数据类型,使用省略号(...) 来表示响应任何类型的 throw,例如:

try {
//...
} catch (...) {
    cout << "division by zero!" << endl;
}

这样当 try 块 throw 一个 exception 时,无论 throw 类型为什么都会匹配到此通用 catch 块。

Files 文件处理

c++ 另一个常用的功能就是对文件的读写操作,需要用到 c++ 标准库:`'

在 fstream 中定义了三种数据类型:

  • ofstream:输出文件流,用来创建或写入文件
  • ifstream:输入文件流,用来读取文件信息
  • fstream:通用文件流,包含 ofsteam 和 ifstream 的内容,支持创建,读入,写入文件。

在 c++ 中对文件操作需要 include iostreamfstream:

#include <iostream>
#include <fstream>

对文件操作的一些 class 直接或间接继承自 istream 和 ostream,例如在上一节用到的 cin 就是 istream class 的一个 object,cout 是 ostream class 的一个 object。

打开文件及写入

在对文件进读写前,需要首先打开它。

ofstream 和 fstream 的 object 都可以用来打开文件然后进行写操作,我们打开一个 test.txt 文件然后写入内容最后关闭文件:

#include <iostream>
#include <fstream>
using namespace std;

int main() {
    ofstream MyFile;
    MyFile.open("test.txt");
    MyFile << "some text.\n";
    MyFile.close();

    return 0;
}

以上代码创建一个 ofstream 类型的 object,使用 open function 打开一个文件,然后给这个文件写入内容,最后关闭文件。如果此文件不存在则 open function 会自动创建。可以看到使用了和之前操作 iostream 同样的 stream 流操作符 <<

open function 打开的文件可以是一个路径,如果只有文件名则默认在程序根目录。

也可以使用 ofstream 的构造器直接给 object 初始化定义文件路径:

int main(int argc, const char * argv[]) {

    ofstream MyFile("test.txt");
    MyFile << "some text.\n";
    MyFile.close();
    
    return 0;
}

使用 open function 的区别是可以定义一个文件的绝对路径,可以和程序不在一个目录。

在特定情况下,使用 open function 打开文件会无效,如:没有权限打开文件。这时候可以使用 is_open function 来确认文件是否已经被正确打开且可以被访问:

int main(int argc, const char * argv[]) {

    ofstream MyFile("test.txt");
    if (MyFile.is_open()) {
        MyFile << "some text.\n";
    } else {
        cout << "somethin went wrong" << endl;
    }
    MyFile.close();
    
    return 0;
}

is_open function 检查文件是否被正常打开,返回值为:true 或 false。

open function 的第二参数可以用来定义文件打开模式,一下是支持的模式列表:
1.jpeg

以上的 flag 标记可以结合起来使用,用或操作符| 来分隔。例如使用 write 模式同时 truncate 文件,使用一下语句:

ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc );

读取文件

类似于写文件,读取文件需要一个 ifstream 或 fstream 的 object。示例如下:

string line;
ifstream ReadFile("test.txt");
if (ReadFile.is_open()) {
    while (getline(ReadFile, line)) {
        cout << line << endl;
    }
} else {
    cout << "fail to open file" << endl;
}

首先创建一个 ifstream class 的实例 ReadFile并初始化文件地址。如果文件打开正常则使用 getline function 来一行行的读取文件内容到字符串 line,然后打印到输出。

getline function 属于 istream class,会逐行读取来自 istream 输入流的内容到一个字符串变量,每次执行都会自动换行定位到下一行输入流 istream 的内容直到结尾会跳出 while 循环。

]]>
<![CDATA[C++ 入门教程之七 -- 继承和多态]]> https://blog.niekun.net/archives/1927.html 2020-11-21T22:31:00+08:00 2020-11-21T22:31:00+08:00 admin https://niekun.net inheritance 继承

inheritance 继承是面向对象编程最重要的概念之一。继承允许我们基于已有的 class 创建新的 class。这能够极大的方便我们创建应用程序。

一个 class 的属性被其他 class 继承,我们称这个 class 叫做 base class。一个从 base class 继承属性的 class 我们称其为 derived class。一个 derived class 获得所有 base class 的功能,且可以有自己独有的功能:
1.png

继承的概念是 is a 属于是什么 的关系,例如:猫 is a 动物,狗 is a 动物。两者都继承了动物的属性。

我们来建立两个 class:Mother 和 Daughter:

class Mother
{
 public:
 Mother() {};
 void sayHi() {
   cout << "Hi";
 } 
};

class Daughter 
{
 public: 
 Daughter() {};
};

Mother 有一个 sayHi() function。下面我们让 Daughter 继承 Mother 的属性,Daughter class 名称修改如下:

class Daughter : public Mother
{
 public: 
 Daughter() {};
};

使用一个冒号: 标记 base class。access specifier 访问标记符 public 表示所有 base class 里 public 段下的内容继承给 derived class 里的 public 段下。

以上示例中 Daughter 继承了所有 Mother 的 public 下的元素,所以我们可以实例化一个 Daughter 的 object 然后调用 sayHi() function 了:

#include <iostream>

using  namespace std;

class Mother
{
 public:
 Mother() {};
 void sayHi() {
   cout << "Hi" << endl;
 }
};

class Daughter: public Mother
{
 public:
 Daughter() {};
};

int main(int argc, const char * argv[]) {
    Daughter d;
    d.sayHi();
    return 0;
}

关于 access 访问标记符的使用,在下面会详细介绍使用方法。

一个 derived class 可以继承所有 base class 的 function 出过一下几种特殊情况类型:

  • constructor and destructor 构造器和销毁器
  • 复用操作符 function
  • friend function

一个 class 可以同时继承多个 base class,使用逗号, 分隔 base class,如:class Daughter: public Mother, public Father

protected 元素

对于 class 中的元素的 access 访问表示符,我们前面使用了 public 和 private。public 里的元素可以被外界访问或修改,private 里的元素只能被 class 内部使用,或者通过 friend function 来读取。

除了以上两种识别符外,还有一种类型:protected

在 protected 内的元素和 private 段内的很类似。唯一的区别是,protected 内的元素可以被 derived class 访问。

例如我们给 Mother 添加一个 protected 元素:

class Mother {
 public:
 void sayHi() {
   cout << var;
 }

 private:
 int var=0;

 protected:
 int someVar;
};

someVar 变量可以被 Daughter 访问。

继承的类型

access 访问标记符能够用来定义继承类型。

在上面的示例中我们使用 public 作为访问标记符:class Daughter: public Mother。private 和 protected 同样可以被使用。

三者的区别:

  • Public Inheritance: base class 里 public 段内的元素继承到 derived class 里的 public 段内,base class 里 protected 段内的元素继承到 derived class 里 protected 段内,base class 里 private 段内的元素永远不能能够被 derived class 直接访问,但可以通过 public 或 protected 内的 function 来间接获取数据。
  • Protected Inheritance: base class 里 public 和 protected 段内的元素继承到 derived class 里的 protected 段内。
  • Private Inheritance: base class 里 public 和 protected 段内的元素继承到 derived class 里的 private 段内。

public 继承类型在继承中是最常用的。如果在程序中没有指定某个继承类型,则默认为 private

构造器和销毁器

上面提到了 derived class 不会继承 base class 的构造器和销毁器。但是 derived class 的实例 object 在被建立和销毁时,base class 的构造器和销毁器会被自动调用。

我们建立 Mother class:

class Mother
{
 public:
 Mother() {
   cout << "mother's constructor" << endl;
 }
 ~Mother() {
   cout << "mother's destructor" << endl;
 }
};

int main(int argc, const char * argv[]) {
    Mother m;
    return 0;
}

实例化 Mother 后,输出如下:

mother's constructor
mother's destructor

然后我们建立 Daughter class:

class Daughter: public Mother
{
 public:
 Daughter() {
    cout << "daughter's constructor" << endl;
 }
 ~Daughter() {
   cout << "daugther's destructor" << endl;
 }
};

int main(int argc, const char * argv[]) {
    Daughter d;
    return 0;
}

实例化 Daughter 后,输出如下:

mother's constructor
daughter's constructor
daugther's destructor
mother's destructor

可以看到当 Daughter 的 object 创建时,首先 base class 的构造器被调用,然后调用 derived class 的构造器。
当 object 销毁时,首先 derived class 的销毁器被调用,然后 base class 的销毁器被调用。

你可以理解为 derived class 依赖于 base class 才能工作,所以 base class 需要首先被执行。

多态性

polymorphism 多态化意思是:拥有多种样式。通常多态化出现在那些有着继承关系的 class 中。在 c++ 中多态性意思是在调用同一个 function 时随着 object 类型的不同而有着不同的执行效果。

下面举例说明:我们创建一个游戏,有两种人物:monster 怪物和 ninja 忍者。他们都有一个共同的 function:attack,但是两者攻击的模式是不同的。在这个场景下,多态性能够实现不同的 objects 调用同样的 attack function 而有不同的实现效果。

首先创建一个 Enemy class:

class Enemy {
 protected: 
 int attackPower;

 public:
 void setAttackPower(int a){
  attackPower = a;
 }
};

Enemy class 有一个public function:setAttackPower 来设置 protected 里的参数:attackPower。

然后我们建立两个 derived class 以 Enemy 为 base class,各自有独立的 attack function:

class Ninja: public Enemy {
 public:
 void attack() {
   cout << "Ninja! - "<<attackPower<<endl;
 }
};

class Monster: public Enemy {
 public:
 void attack() {
   cout << "Monster! - "<<attackPower<<endl;
 }
};

在 main 中实例化:

int main() {   
 Ninja n;
 Monster m;  
}

由于 Monster 和 Ninja 都继承自 Enemy,所以他们的实例 object 也都是 Enemy 类型的 object。我们可以如下定义:

Enemy *e1 = &n;
Enemy *e2 = &m;

我们定义两个 Enemy 类型的指针指向两个 object。

在 main 中调用对应的 function:

int main(int argc, const char * argv[]) {
    Ninja n;
    Monster m;
    Enemy *e1 = &n;
    Enemy *e2 = &m;
    
    e1->setAttackPower(20);
    e2->setAttackPower(80);
    
    n.attack();
    m.attack();
    return 0;
}

两个指针都是 Enemy 类型的,所以可以用来使用 Enemy 中的 function 设置参数,然后分别调用 derived class 的 object 的 attack function。注意这里没法用这两个指针直接调用 attack(),因为 Enemy 中并没有定义这个 function。

虚拟 function

上面的示例中,我们使用 Enemy 类型的指针指向了 derived class 的 object。但是我们无法使用这个指针调用 attack function,因为在 base class 中不包含这个 function。为了实现这个功能,需要在 base class 中定义一个 attack 的 virtual function

virtual function 的意义就是在 base class 里定义一个 function,在 derived class 内 override 重写这个 function,这样就可以实现多态化,使用 base class 类型的指针调用同一个 function 根据 derived class 的不同而实现不同的功能。

使用关键词 virtual 定义 virtual function:

class Enemy {
    protected:
    int attackPower;
    
    public:
    virtual void attack() {}
    void setAttackPower(int a){
        attackPower = a;
    }
};

在 Enemy class 中我们定义一个 attack 的 virtual function,然后在两个 derived class 中重写这个 function 使其有着不同的内容。

现在我们就可以使用 Enemy 类型的指针调用 attack function了:

int main(int argc, const char * argv[]) {
    Ninja n;
    Monster m;
    Enemy *e1 = &n;
    Enemy *e2 = &m;
    
    e1->setAttackPower(20);
    e2->setAttackPower(80);
    
    e1->attack();
    e2->attack();
    return 0;
}

virtual function 类似于一个模版,告诉 derived class 可以定义属于自己的这个 function 功能。指针在调用时会根据 object 属于哪个 derived class 而去具体完成指令。

一个包含有 virtual function 的 class 称之为多态化的 class

当然在 base class 内的 virtual function 也可以给其具体的指令,我们给 Enemy class 里的 virtual attack function 定义内容:

class Enemy {
    protected:
    int attackPower;
    
    public:
    virtual void attack() {
        cout << "Enemy! - " <<attackPower << endl;
    }
    void setAttackPower(int a){
        attackPower = a;
    }
};

然后创建一个 Enemy 的 object 及其指针,尝试调用其 attack function:

int main(int argc, const char * argv[]) {
    Ninja n;
    Monster m;
    Enemy e;
    
    Enemy *e1 = &n;
    Enemy *e2 = &m;
    Enemy *e3 = &e;
    
    e1->setAttackPower(20);
    e2->setAttackPower(80);
    e3->setAttackPower(30);
    
    e1->attack();
    e2->attack();
    e3->attack();
    return 0;
}

输出如下:

Ninja! - 20
Monster! - 80
Enemy! - 30

这就是多态化 class 的优势,不同的 derive class 使用同一名称的 function 在不同的场景下执行不同的指令。

abstract class

当我们在 base class 中定义 virtual function 时,在 base class 中并没有此 function 具体要实现的内容,只有在 derived class 中才需要做定义。这种情况下,可以将 base class 中的 virtual class 定义为 pure virtual functions 纯粹的虚拟 function 而不需要定义它的任何内容。使用 =0 来进行表示:

class Enemy {
 public:
  virtual void attack() = 0;
}; 

如果 base class 中定义了 pure virtual functions,那么在 derived class 中必须重写此 function,否则在实例化 derived class 时编译器会报错。

这种包含有 pure virtual functions 的 class 叫做 abstract class 抽象化的 class,这种 class 不能直接实例化 object,会产生报错。必须实例化其 derived class 且重写了 virtual function。

现在实例化 Enemy 的 object 会报错:

Enemy e; // Error

这种 pure virtual functions 的好处是,我们可以直接定义 base class 类型的指针指向不同的 derived class object,然后使用同样的 function 名称来执行不同的指令。

]]>
<![CDATA[C++ 入门教程之六 -- Classes 实践/头文件/操作符复用]]> https://blog.niekun.net/archives/1920.html 2020-11-21T16:42:00+08:00 2020-11-21T16:42:00+08:00 admin https://niekun.net 下面我们通过实际应用来介绍如果在真实环境下应用。

我们使用 Code::Blocks IDE 来进行调试。这是一个免费的 c++ 开发环境,能够应用于绝大多数使用场景。

官网:http://www.codeblocks.org/

在下载界面下载对应系统版本的安装包:http://www.codeblocks.org/downloads/26

最好安装带编译器的版本,这样就可以直接使用。

新建项目

安装好 codeblock 后新建一个项目:
1.jpg

选择 console application:
2.jpg

根据提示设置项目名称等,完成项目建立。

然后添加一个 class:
3.jpg

设置 class 名称,取消勾选 has destructor,勾选 headers and implementation file shall be in same folder
4.jpg

完成后项目结构如下:
5.jpg

  • main.cpp 项目主文件
  • MyClass.cpp class source 源文件
  • MyClass.h class header 头文件

头文件和源文件

头文件 .h 申明了 class 的 function 和 variable 变量等元素,我们新建的 class 头文件内容如下:

#ifndef MYCLASS_H
#define MYCLASS_H


class MyClass
{
    public:
        MyClass();

    protected:

    private:
};

#endif // MYCLASS_H

里面只默认包含了一个 constructor 构造器 MyClass()

源文件 .cpp 包含了这个 class 具体 function 和 variable 等元素的实现过程。当前只有一个空的构造器 function:

#include "myclass.h"

MyClass::MyClass()
{
    //ctor
}

范围解析符:
我们注意到源文件内构造器前的两个冒号::,叫做 scope resolution operator 范围解析符。这个符号是用来表示那些已经在头文件内申明过的 function 要在这里做具体实现了。用来和头文件内定义的元素进行关联的作用。

关于::.的使用场景我的理解是,当在定义一个元素或功能时用范围解析符::,当实例化object后使用其一个元素或功能时用点.,例如:

//定义一个元素为class内的一个att1类型:
MyClass1::att1 a;

//使用class的function:
MyClass2 obj;
obj.func();

要在 main 总使用我们创建的 class,只需要在其中引用头文件名即可:

#include <iostream>
#include "myclass.h"

using namespace std;

int main()
{
    MyClass obj;
    return 0;
}

总体来说,头文件用来定义 class 提供了哪些功能及元素,源文件用来具体实现这些功能。

destructor 销毁器

和上一章我们讲到的 constructor 构造器类似,destructor 销毁器也是一个特殊的 function,他在 object 被销毁时自动执行。

销毁一个 object 一般是在跳出创建这个 object 的 scope 段或者使用 pointer 指针指向这个 object,使用 delete 指令清除此指针指向的数据。

销毁其的写法和构造器类似,使用 class 名作为 function 名字,前面加上一个波浪符~。当然也是没有返回值类型的。例如:

class MyClass {
  public: 
    ~MyClass() {
     // some code
    }
};

使用销毁器能够方便的实现在关闭程序时释放资源,关闭文件,释放内存等功能。

在头文件中声明一个销毁器:

class MyClass
{
  public:
   MyClass();
   ~MyClass();
};

然后再源文件内定义具体实现内容:

#include "MyClass.h"
#include <iostream>
using namespace std;

MyClass::MyClass()
{
  cout<<"Constructor"<<endl;
}

MyClass::~MyClass()
{
  cout<<"Destructor"<<endl;
}

不同于构造器,销毁器不能使用参数,也不能被重写,也就是一个 class 只能有一个销毁器。销毁器不是必须有的,不需要的话可以不写

返回我们的 main,我们已经定义了一个 object MyClass obj,编译运行程序,会在终端输出以下:

Constructor
Destructor

选择符

使用选择符 -> 可以访问一个指针指向的 object 的元素。例如:

MyClass obj;
MyClass *ptr = &obj;
ptr->myPrint();

我们创建了 object obj 然后定义一个指针 ptr 指向这个 object 的地址,然后我们可以使用选择符来访问 object 的元素。

object 元素访问基本原则:

  • 如果直接面对 object,使用. 来访问 object 的元素,如:obj.myPrint()
  • 如果使用面对指针指向的 object,使用选择符-> 来访问 object 的元素,如:ptr->myPrint()

constants 常数

常数就是一个有固定值的表达式,它的只在程序运行期间不能够被改变,使用关键词const 定义一个有固定值的变量:

const int a=2;

注意所有 const 类型的变量必须在创建时给其赋值。

我们可以创建一个 const 类型的 object:

const MyClass obj;

object 内所有的 variable 变量必须在初始化时赋值,一般在 constructor 构造器内完成,实例化的时候直接给其传递数据。如果没有提供构造器完成参数初始化赋值,会引起编译器报错。

一旦一个 const 类型的 object 被创建,它内部的所有 variable 变量的值都不可以被改变了,包括直接修改 public 段的变量或者使用 function 修改 private 的变量都不可以。

只有非 const 的 object 才可以调用非 const 的 function。对于 const 的 object 不能只能调用 const 的 function,定义一个 const 的 function 只需要在后面加上关键词 const,头文件定义示例如下:

class MyClass
{
  public:
    void myPrint() const;
};

源文件同样的方式:

#include "MyClass.h"
#include <iostream>
using namespace std;

void MyClass::myPrint() const {
  cout <<"Hello"<<endl;
}

然后我们就可以实例化一个 const 的 object 使用厘米俺的 const function:

int main() {
  const MyClass obj;
  obj.myPrint();
}
// Outputs "Hello"

在一个 const object 调用常规的 function 会引起报错。同时在 const function 内尝试修改 object 内某变量数据也会报错。

元素初始化器

const 类型的元素数据不能够被改变,且必须在创建时赋值。c++ 提供了一个语法结构来给 class 内元素进行初始化叫做:constructor initializer list 构造器初始化列表。

以下示例执行会引起报错,因为在 function 中操作的元素有 const 类型的元素:

class MyClass {
  public:
   MyClass(int a, int b) {
    regVar = a;
    constVar = b;
   }
  private:
    int regVar;
    const int constVar;
};

const 类型元素在申明后不能再对其修改。

这时候就需要使用 构造器初始化列表来对其初始化赋值:

class MyClass {
 public:
  MyClass(int a, int b)
  : regVar(a), constVar(b)
  {
  }
 private:
  int regVar;
  const int constVar;
};

需要初始化的元素列表下载构造器后,前面使用一个冒号:,每个元素间使用逗号, 分隔。使用语法 variable(value) 来对其赋值。结尾不需要加分号;

使用构造器初始化列表可以用来避免将需要初始化的 const 类型参数放在其 body 内处理而引起错误。

修改我们的项目文件:

MyClass.h:

#ifndef MYCLASS_H
#define MYCLASS_H


class MyClass
{
    public:
        MyClass(int a, int b);
        ~MyClass();

    protected:

    private:
        int regVar;
        const int constVar;
};

#endif // MYCLASS_H

MyClass.cpp:

#include "myclass.h"
#include <iostream>

using namespace std;

MyClass::MyClass(int a, int b)
: regVar(a), constVar(b)
{
    cout << regVar << endl;
    cout << constVar << endl;
}

MyClass::~MyClass()
{
    cout << "destructor" << endl;
}

main.cpp:

#include <iostream>
#include "myclass.h"

using namespace std;

int main()
{
    MyClass obj(42, 33);

    return 0;
}

输出结果为:

42
33
destructor

注意构造器初始化列表也可以应用于常规变量,但是必须应用于 const 变量。

结构化

在真实世界中一个 object 可能是很复杂的,由很多其他简单的 objects 构成。例如一辆汽车的组成部分有车身,发动机,轮胎等。这一组合过程叫做结构化。

在 c++ 中,一个 class 可能作为另一个 class 的元素。下面的例子中我们构建两个 class:Person 和 birthday,且 Person 中包含 Birthday 作为一个元素。

birthday.h:

#ifndef BIRTHDAY_H
#define BIRTHDAY_H

class Birthday
{
    public:
        Birthday(int m, int d, int y);
        void printDate();

    protected:

    private:
        int month;
        int day;
        int year;
};

#endif // BIRTHDAY_H

birthday.cpp: 注意将 function 返回类型放在范围解析符前

#include "birthday.h"
#include <iostream>

using namespace std;

Birthday::Birthday(int m, int d, int y)
: month(m), day(d), year(y)
{
}

void Birthday::printDate()
{
    cout << month << "/" << day << "/" << year << endl;
}

persion.h:

#ifndef PERSON_H
#define PERSON_H
#include "birthday.h"
#include <string>

using namespace std;

class Person
{
    public:
        Person(string n, Birthday b);
        void printInfo();

    protected:

    private:
        string name;
        Birthday bd;
};

#endif // PERSON_H

persion.cpp:

#include "person.h"
#include <iostream>

using namespace std;

Person::Person(string n, Birthday b)
: name(n), bd(b)
{
}

void Person::printInfo()
{
    cout << name << endl;
    bd.printDate();
}

Person 包含 name 和 Birthday 两个元素,且在构造器内对其初始化。

结构化的意义是建立一种包含关系,即 Person 中包含一个 Birthday。在 Person 中有一个 printInfo() 来输出信息,bd 是 Birthday 的实例,所以可以直接使用其 function。

在 main 中实例化测试:

#include "person.h"
#include "birthday.h"

using namespace std;

int main()
{
    Birthday bd(11, 25, 1989);
    Person p("Marco", bd);
    p.printInfo();

    return 0;
}

以上示例中,我们首先创建一个 Birthday 的 object 并初始化参数,然后创建一个 Person 的 object 并初始化参数,其中第二个参数使用第一步创建的 object 作为数据,最后调用 Person object 的 function 来输出信息。

结构化的优势是保持每个 class 相对简单,专注于一个任务。同时让每个各个 object 保持独立性,和可复用性。

friend 关键词

位于 private 段的 class 里的元素默认无法被外界直接访问,但可以通过在 class 内申明一个并非 class 内部的 friend function 来实现对 private 元素的读取。

使用关键词 friend 来定义,在 MyClass 头文件中修改如下:

#ifndef MYCLASS_H
#define MYCLASS_H

class MyClass
{
    public:
        MyClass(int a, int b);
        ~MyClass();

    protected:

    private:
        int regVar;
        const int constVar;

    friend void readPriv(MyClass *obj);
};

#endif // MYCLASS_H
};

我们在 class 中申明一个 function,传入参数为 class 本身,注意传入的模式是 by reference 也就是传入 object 的地址。详细说明参考:C++ 入门教程 -- Functions

然后再外部定义 someFunc 的内容,我直接在 main 中定义,并测试读取 private 参数:

#include "myclass.h"
#include <iostream>

using namespace std;

void readPriv(MyClass *obj)
{
    cout << obj->constVar << endl;
}

int main()
{
    MyClass a(42, 33);
    MyClass *b = &a;
    readPriv(&a);

    return 0;
}

输出结果为:

33

再定义 friend function 时,传入 by reference 参数可以写成 *obj 也可以写成 &obj。只是在后续 function 内容中使用 . 还是 -> 调用内部功能。然后再调用 friend function 时是传入 object 地址还是直接传入 object 本身。

friend function 不属于任何 class。

this 关键词

每个 c++ object 都有一个指向本身的指针叫做:this。在 object 内部 function 中,可以通过使用 this 指针来引用 object 自身。

我们建立一个简单的 class:

class MyClass {
 public:
  MyClass(int a)
  : var(a)
  { }
  void printInfo() {
   cout << var << endl;
   cout << this->var << endl;
   cout << (*this).var << endl; 
  }
 private:
  int var;
};

printInfo() 中的三条指令会得到相同的结果。this 是一个指针,所以使用 -> 来读取对应地址的数据,也可以使用 *this 数据查询符来表示此地址下的数据。可以参考教程:C++ 入门教程 -- 数据类型,数组及指针

操作符复用

大多数 c++ 中的操作符都可以被重新定义。因此让我们自定义的数据类型进行运算操作,比如将两个 object + 加起来。

以下列表中的操作符可以被复用:
1.png

不能被复用的符号有:: | .* | . | ?:

我们定义一个简单的 class:

class MyClass {
 public:
  int var;
  MyClass(int a)
  : var(a)
  { }
};

下面示例我们将重新定义加号+ 来让两个 object 可以加起来。

定义复用操作符其实是一个 function,通过关键词 operator 和操作符来定义,类似于普通 function,这里也有返回类型和传入数据项需要定义,以下示例中,我们复用加号+,然后返回值为一个此 class 本身的 object,传入数据为一个 object 地址:

class MyClass {
 public:
  int var;
  MyClass(int a)
  : var(a) { }

  MyClass operator+(MyClass &obj) {
   MyClass res;
   res.var = this->var+obj.var;
   return res; 
  }
};

再复用操作符 function 中,我们定义了一个 新的 object,然后使用 this 调用此 object 本身的参数 var 和传入 object 的 var 相加,结果放入新建立的 object 的 var 内,最后返回这个新 object。

这样定义后,我们就可以直接使用操作符来做运算了:

int main() {
  MyClass obj1(12), obj2(55);
  MyClass res = obj1 + obj2;

  cout << res.var;
}

//Outputs 67

以上就是 class 的基本使用方法。

]]>
<![CDATA[C++ 入门教程之五 -- Classes and Objects]]> https://blog.niekun.net/archives/1918.html 2020-11-20T11:32:00+08:00 2020-11-20T11:32:00+08:00 admin https://niekun.net object 对象

Object Oriented Programming 面向对象编程是为了让编程更加接近于真实世界的理解方式。在程序中,每个 object 是一个独立的 unite 单元,拥有自己的 identify 标识,就像真实世界的某个独立物体一样。

例如一个苹果就是一个 object,它的 identify 就是名称 苹果,每个苹果都有自己独立的 attributes 属性,如颜色,大小。一个属性就是这个 object 当前状态的描述。不同 object 的 attributes 属性是不一样的,例如一个苹果是绿色的,另一个是红色的。

在正式世界中 object 都有其 behave 行为,例如汽车的 move 移动,手机的 ring 响铃。这种 object 的行为叫做 object 的 type 类型。

描述一个 object需要的元素:identify 标识,attributes 属性,behavior 行为

在程序中每个 object 是独立的,拥有独立的 identify 用来区分其他的 object:
1.png

class

我们通过创建 class 来表示 object,一个 class 描述了一个 object 的形象,但是它并不是一个真正的 object,他只是一个对某种 object 结构的定义。一个 class 可以用来建立多个 object。例如一套设计图纸可以用来作为蓝图修建多栋楼房。

一个 class 包含:identify,attributes,behavior

程序中,一个对象的 type 就是 class 的 name 名称;Attributes 属性可以是 properties 或 data 数据;behavior 行为通常是一个 function。

例如我们建立一个银行系统程序:

  • name: BankAccount
  • attributes: accountNumber, balance, dateOpened
  • behavior: open(), close(), deposit()

一个 class 定义了某种 object 需要的属性和行为。但是它并不直接定义具体的属性值是多少,她只是一个框架的描述。

当我们写好了一个 class,可以基于这个 class 来创建 objects,这个 object 就是 instance of class,就是 class 的实例。

建立 class

使用关键字 class 来建立 class,然后定义 class 名称,class 内容写在大括号{} 内。注意每个 class 结尾必须写分号;。例如:

class BankAccount {

};

一个 class 的 attributes 和 behaviors 可以设置 access 访问级别。定义时使用关键词 public 不仅可以在 class 内部使用,也可以在 class 外部访问这个属性。也可以是使用关键词 privateprotected,下面做详细介绍。

建立一个 class:

class BankAccount {
  public:
    void sayHi() {
      cout << "Hi" << endl;
    }
};

然后 instance 实例化这个 class:

int main() 
{
  BankAccount test;
  test.sayHi();
}

实例化的 object test 拥有其 class 所有的属性和行为。使用点分割符. 来访问 object 的各种属性和 function。

抽象化

数据抽象化的理念是给外部环境提供最核心的信息,而不用提供具体的细节。比如我们抽象化一本书,我们不用知道他具体有多少也,多少个字,什么颜色。我们只需要知道它是一本书就行了。

抽象化的理念是面向对象编程最基础的模块。可以让我们建立一个 class 模型,然后基于这个模型创建具体的 objects 对象。

封装

encapsulation 封装意味着将一个整体包围起来,不仅仅是将其内容放在一起,也可以将其保护起来。它的设计原则就是让外部程序只能够访问其开放的元素,其他内容保持隐藏状态。

例如我们上面的 BankAccount class,我们不想要外部直接访问修改 balance 余额属性,我们需要其使用 deposit()withdraw() 方法来对其进行操作。所以我们需要将 balance 属性对外隐藏掉,只能通过内部 function 来访问。

封装的优势有:

  • 控制内部数据的访问和修改
  • 代码更加灵活,方便后续根据情况修改
  • 修改一个地方,不影响其他地方

下面举例说明如何使用封装来控制内部数据的可访问性,使用 public,private,protected 关键词。

注意如果没有使用关键词,默认 class 内所有都是 private 类型的。

访问 public 的数据:

#include <iostream>
#include <string>
using namespace std;

class myClass {
  public:
    string name;
};

int main() {
  myClass myObj;
  myObj.name = "SoloLearn";
  cout << myObj.name;
  return 0;
}

//Outputs "SoloLearn"

使用 public 关键词定义可被外部访问的属性,注意关键词后的冒号:

使用 private 保护内部数据:

#include <iostream>
#include <string>
using namespace std;

class myClass {
  public:
    void setName(string x) {
      name = x;
    }
  private:
    string name;
};

int main() {
  myClass myObj;
  myObj.setName("John");

  return 0;
}

name 不可以被外部直接访问修改,但是通过 setName() 就可以间接修改 name 的值。

也可以通过 function 间接读取 private 的某些属性:

class myClass {
  public:
    void setName(string x) {
      name = x;
    }
    string getName() {
      return name;
    }
  private:
    string name;
};

以上示例,通过建立 public 里的 getName() 方法来读取 name 的值。

constructor 构造器

constructor 是 class 中特殊的 function,这个 function 的名称和 class 名称一样且没有返回类型,甚至没有 void,它会在 instance 实例化 object 时自动被执行,例如:

class myClass {
  public:
    myClass() {
      cout <<"Hey";
    }
    void setName(string x) {
      name = x;
    }
    string getName() {
      return name;
    }
  private:
    string name;
};

int main() {
  myClass myObj;

  return 0;
}

//Outputs "Hey"

以上示例中,在实例化 myObj 时,会自动执行构造器 function。

构造器 function 可以方便的让我们在实例化 class 时设置 initial 初始化参数。默认构造器没有参数,如果需要的话我们可以加入参数。例如:

class myClass {
  public:
    myClass(string nm) {
      setName(nm);
    }
    void setName(string x) {
      name = x;
    }
    string getName() {
      return name;
    }
  private:
    string name;
};

int main() {
  myClass ob1("David");
  myClass ob2("Amy");
  cout << ob1.getName();
}
//Outputs "David"

以上示例中,构造器的作用是使用一个参数给 private name 赋值。当实例化这个 class 时,需要传入构造器需要的参数。

注意我们可以在一个 class 中建立多个 constructor 构造器,来使用不同的参数。

]]>
<![CDATA[C++ 入门教程之四 -- Functions]]> https://blog.niekun.net/archives/1915.html 2020-11-20T09:38:00+08:00 2020-11-20T09:38:00+08:00 admin https://niekun.net 一个 function 是为了完成某个任务的许多命令的一个集合。使用 function 的优势有:

  • 代码复用
  • 独立测试某功能
  • 修改一个 function 内部代码不影响程序整体结构
  • 同一个 function 可使用不同传入参数调用

一个有效的 c++ 程序至少要有一个 function:main()

返回类型

mian() function 通常结构如下:

int main()
{
  // some code
  return 0;
}

function 的返回值类型定义在它的名称前,以上示例中,返回类型为 int 型,表明此 function 会返回一个 int 型数据。有些 function 在执行后不需返回数据,则使用 void 来定义。

void 是一个基本数据类型,用来定义无值 valueless 申明。

结构

function 定义结构如下:

return_type function_name( parameter list )
{
   body of the function
}
  • return-type: 返回值数据类型
  • function name: function 名称
  • parameters: 传递参数,当此function被调用时,传递数据给 function 内部使用。需要指定类型,名称及个数
  • body of the function: 指令集合

parameters 参数是可选的,当不需要传递参数时可以留空。

使用

我们定义一个无返回值类型的 function:

void printSomething() 
{
  cout << "Hi there!";
}

此 function 功能为输出一个字符串。

我们在 main() 中调用此 function:

int main() 
{
   printSomething();
   
   return 0;
}

调用某个 function 只需要使用 function 名称及传入参数即可。

注意 function 的申明和调用顺序,需要先申明然后调用,不然会报错:

#include <iostream>
using namespace std;

void printSomething() {
  cout << "Hi there!";
}

int main() {
  printSomething();

  return 0;
}

一个 function 的申明告诉编译器这个 function 的名称及调用方法,其内容可以在后续代码中定义:

#include <iostream>
using namespace std;

//Function declaration
void printSomething();

int main() {
  printSomething();

  return 0;
}

//Function definition
void printSomething() {
  cout << "Hi there!";
}

传递参数

如果一个 function 需要传递数据,需要在申明定义正式的参数用来接收传递数据值。例如:

void printSomething(int x) 
{
   cout << x;
}

以上定义一个 function 接收一个 int 整数然后输出这个整数。

function 内的变量在调用此 function 时创建,在结束时清除。

当一个带传递参数的 function 被定义后,在调用时需要传入对于数据类型的数据:

#include <iostream>
using namespace std;

void printSomething(int x) {
  cout << x;
}

int main() {
  printSomething(42);
}

// Outputs 42

以上示例将 42 传入 printSometing() 然后输出数据。

在 function 内对传入参数数据的改变不会对影响到外部的数据。

可以在调用时使用不同的传入数据:

int main() {
  printSomething(42);
  printSomething(22);
  printSomething(36);
}

可以定义多个传递参数,使用逗号, 分隔,如:

int addNumbers(int x, int y) {
  int result = x + y;
  return result;
}

int main() {
  cout << addNumbers(50, 25);
  // Outputs 75
}

以上示例定义了两个传递参数,返回值为两个参数的和。

rand() function

创建一个随机整数是程序内常用的使用场景,可以调用 rand() function 获取随机整数。需要用到 <cstdlib> 库:

#include <iostream>
#include <cstdlib>
using namespace std;

int main() {
  cout << rand();
}

输出 10 个 1 - 6 之间的随机数:

int main () {
  for (int x = 1; x <= 10; x++) {
  cout << 1 + (rand() % 6) << endl;
  }
}

/* Output: 
6
6
5
5
6
5
1
1
5
3
*/

但是 rand() 其实生成的是伪随机数,因为每次执行以上程序生成的数都是一样的。使用 srand() 可以获得真正的随机数,它支持设置一个 seed 种子值来作为 rand() 的运行算法:

int main () {
  srand(98);

  for (int x = 1; x <= 10; x++) {
    cout << 1 + (rand() % 6) << endl;
  }
}

改变 srand() 的 seed 传入值可以改变 rand() 算法来生成新的随机数。所以同一个 seed 值执行后生成的随机数是一样的。

那么如何使用不同的 seed 值呢,一个方法是读取当前时间来作为 seed,这样就可以得到真正的随机数,读取系统时间需要使用 <ctime> 库:

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

int main () {
  srand(time(0));

  for (int x = 1; x <= 10; x++) {
    cout << 1 + (rand() % 6) << endl;
  }
}

time(0) 返回当前时间的秒位。

传递参数默认值

当定义一个 function 时可以给参数项设置一个 default 默认值,这样在调用这个 function 时如果没有给参数赋值,就会使用默认值的数据,例如:

int sum(int a, int b=42) {
  int result = a + b;
  return (result);
}

以上示例中给变量 b 设置默认值 42,当调用这个 function 时如果没有给 b 传入数据,就会使用默认值:

int main() {
  int x = 24;
  int y = 36;

  int result = sum(x, y);
  cout << result << endl;
  //Outputs 60

  result = sum(x);
  cout <<  result << endl;
   //Outputs 66

  return 0;
}

overload function 重写

function overload 功能支持使用同样的名称建立多个 function,拥有不同的传递参数。

例如创建一个 function,有一个 int 型的参数:

void printNumber(int a) { 
  cout << a;
}

我们可以再次创建一个有同样名称的 function 有一个 float 类型的参数:

void printNumber(float a) { 
  cout << a;
}

这两个 function 可以同时存在且互不影响,调用时根据传递参数类型或个数的不同来自动判断调用的具体是那个 function。

注意不能仅仅建立有不同的返回值类型的同名 function 例如以下 function 不能同时建立:

int printName(int a) { }
float printName(int b) { }
double printName(int c) { }

递归 recursion

递归操作在程序中很常见,可以在 function 中调用其自身。

递归在数学中很常见,如计算阶乘:

4! = 4 * 3 * 2 * 1 = 24

建立一个 function 实现上面的阶乘计算:

int factorial(int n) {
  if (n==1) {
    return 1;
  }
  else {
    return n * factorial(n-1);
  }
}

int main() {
  cout << factorial(4);
}

如果我们调用此 function 传递参数为 4,则返回值计算过程为:
返回 4 * factorial(3), 然后返回 4*3*factorial(2), 再次返回 4*3*2*factorial(1), 然后为 4*3*2*1,最后 n 为 1 结束递归调用 function。

注意设计 recursion 时一定要有 base case 跳出递归的条件,要不然就会陷入死循环。

数组类型传递参数

数组也可以作为 function 的传递参数类型,例如:

void printArray(int arr[], int size) {
  for(int x=0; x<size; x++) {
    cout << arr[x];
  }
}

int main() {
  int myArr[3]= {42, 33, 88};
  printArray(myArr, 3);
}

printArray() 的参数为 int 型一个数组和单变量,在 main() 中调用时传入一个 3 个元素的数组和数组个数数据。会输出这个数组所有元素。

当调用 function 时传入的数组只需要写数组名即可,不需要方括号[]

传入数据

有两种方式给 function 传入数据:

  • by value:复制传入参数的数据供 function 内部使用,内部对数据的修改操作不影响外部数据
  • by reference:复制传入参数的相关性到 function 内部,这时候对传入数据的操作会直接影响到外部关联的数据

c++ 默认使用 by value 传入数据。如:

void myFunc(int x) {
  x = 100;
}

int main() {
  int var = 20;
  myFunc(var);
  cout << var;
}
// Outputs 20

以上示例中,将 var 的数据传入 myFunc(),传入数据给 function 内部变量 x,内部 x 变量的值修改为 100 后不会影响到外部的 var 的数据。

by reference 方式会将传入数据的内存地址传入 function 中内部参数,这意味着 function 内对传入数据的修改会直接修改源传入数据内容。使用这种方式的 function,需要将传递数据定义为 pointer 指针类型。例如:

void myFunc(int *x) {
  *x = 100;
}

int main() {
  int var = 20;
  myFunc(&var);
  cout << var;
}
// Outputs 100

以上示例中定义一个 function 拥有指针类型的传递参数,在调用 myFunc() 时传入一个数据的地址,这样在 function 内对此地址数据的操作会直接改变外部对应地址的变量的数据。

也可以用另一种形式将数据地址传入 function,使用 & 转换数据地址:

void test2(int &a) {
    a = 2;
}

int main()
{
    int a = 1;
    test2(a);
    cout << a << endl;

    return 0;
}

//OUTPUT:
//2

使用这种方法可以将数据名称直接作为参数传入,function 会自动使用其地址。

通常情况下,使用 by value 方式更加快速和高效,使用 by reference 方式会修改源相关联的数据会占用更多的内存。

]]>
<![CDATA[C++ 入门教程之三 -- 数据类型,数组及指针]]> https://blog.niekun.net/archives/1898.html 2020-11-19T15:54:00+08:00 2020-11-19T15:54:00+08:00 admin https://niekun.net 介绍数据类型

操作系统根据数据类型分配内存空间,同一种数据类型才可以进行计算,如:
合法的运算:

55+15 //  legal C++ expression

非法的运算:

55 + "John" // illegal

数字类型的有两种类型:
整型,如:710
浮点型,如:3.145.67

字符串是一个数字、字符、符号的集合。使用双引号来标记,如:"hello"

字符使用单引号来标记,如:'A'

Boolean 类型只有两个可能的值:truefalse

整型

整型一般默认 4 个字节的存储空间。

使用 int 来定义:

int a = 42;

带符号型,可以定义正数和负数,一般不写默认 int 就是带符号型:

signed int a;

无符号型,只能定义正数型:

unsigned int a;

short 型,一般默认空间占用大小,就是 2 个字节:

short int a;

long 型,2倍默认空间大小:

long int a;

浮点型

有三种浮点类型:float, double, 和 long double。浮点型都是 signed 带符号型的。

一般情况下,float 型 4 个字节,double 型 8 个字节,long double 型 16 个字节。

使用 float 来定义:

float a = 3.14;

字符串

string 字符串类型是一个字符、数字和符号的集合,使用双引号来标记。

使用 string 定义变量,需要使用 <string> 库:

#include <string>
using namespace std;

int main() {
    string test = "hello";
    return 0;
}

注意:<string> 库包含在 <iostream> 库中,所以如果已经引用来 <iostream> 就不需要再引用 <string> 了。

char 类型

char 类型包含了 1 个字节的整数,但是解释器会将这个整数解释为 ASCII 码对应字符。使用单引号来标记,如:

char a = 'a';

变量定义规则

所有变量的定义需要遵守以下规则:

  • 变量名首位必须是一个字母或下划线 _
  • 首字符之后的可以使用字母或数字,不能使用特殊字符或空格

一般有两种常用的定义方法

  • 首字母及每个单词的首字符大写,如: BlankWord
  • 首字母小写后续的每个单词首字符大写,如:blankWord

所有的变量名都是区分大小写的,如:helloword 和 HelloWord 是两个变量。

数组

数组是一个同一种数据类型变量的集合体,只需要一次就可以定义多个同一种变量类型的变量。如:

int a[5] = {1, 2, 5, 7, 8};

变量名后方括号[]定义数组内变量个数,大括号{}内包含数组内元素实际值,使用逗号,分隔各个元素。注意大括号内的元素个数不能超过数组变量定义的个数。

也可以在定义时不说明元素个数:

int a[] = {1, 3, 7, 9};

会自动创建足够存储内部元素的数组。

数组内每个元素都有一个 index 索引,从 0 开始,所以数组内第一个元素索引为 0,第二个为 1。使用索引号可以访问对应元素:

int b[] = {11, 45, 62, 70, 88};

cout << b[0] << endl;
// Outputs 11

cout<< b[3] << endl;
// Outputs 70

如果需要逐个访问数组内元素,可以使用 loop 循环来实现:

int myArr[5];

for(int x=0; x<5; x++) {
  myArr[x] = 42;
}

多维数组

以上是单维数组的定义,也可以定义多维数组,如定义一个二位数组:

int a[3][4];

想象这个数组为一个矩阵,有三行,每行 4 个元素:
DownloadFile.png

定义多维数组定义元素需要每行在一个大括号下单独定义:

int x[2][3] = {
  {2, 3, 4}, // 1st row
  {8, 9, 10} // 2nd row
};

也可以在一行内定义:

int x[2][3] = {{2, 3, 4}, {8, 9, 10}};

使用行号和列号来定位一个元素:

int x[2][3] = {{2, 3, 4}, {8, 9, 10}};
cout << x[0][2] << endl;

//Outputs 4

指针

每个变量都存储在内存单元内,每个内存空间都有地址。这个内存地址可以使用地址查询符&来查询,如:

int score = 5;
cout << &score << endl;

//Outputs "0x29fee8"

返回的地址就是存储变量 score 的值的地址空间。

一个 pointer 指针就是一个存储某个变量所在内存地址的变量。指针类型可以使某些任务实现更加方便,某些任务,如动态内存分配则必须使用指针来实现。

不同类型的指针变量的唯一区别就是其所在内存地址的数据类型的不同。

和其他变量类型一样,定义指针变量需要申明类型。使用星号* 来表示是指针类型的变量。

int *ip;  // pointer to an integer
double *dp;   // pointer to a double
float *fp;  // pointer to a float
char *ch;  // pointer to a character

星号* 可以放在数据类型旁边或者变量旁边或者单独写在中间。

我们可以将一个地址赋给指针变量:

int score = 5;
int *scorePtr;
scorePtr = &score;

cout << scorePtr << endl;

//Outputs "0x29fee8"

scorePtr 的值就是 score 变量的内存地址。

有两种指针的运算器:

  • 地址查询符&,返回操作数所在内存地址 referencing
  • 数据查询符*,返回操作数的内存地址下的数据 dereferencing

例如:

int var = 50;
int  *p;
p = &var;

cout << var << endl;
// Outputs 50 (the value of var)

cout << p << endl;
// Outputs 0x29fee8 (var's memory location)

cout << *p << endl;
/* Outputs 50 (the value of the variable
 stored in the pointer p) */

定义指针类型变量时候的星号* 只是用来表明此变量为指针类型变量,不要和内存查询符混淆。

数据查询符* 指针指向的变量的 alias,例如:

int x = 5;
int *p = &x;

x = x + 4;
x = *p + 4;
*p = *p + 4;

P 是 x 的指针,以上示例的三种运算结果都是一样的,都是将变量 x 的值进行计算。我们可以通过定义变量的指针来操作变量值。

静态内存和动态内存

理解动态内存的工作机制对于开发有很大帮助。再 c++ 中内存被分为两种类型:

  • the stack 静态内存空间:所有的本地变量占用静态内存空间
  • the heap 动态内存空间:再程序运行时动态进行分配的空间,默认不占用

很多情况下,提前并不知道一个定义的变量需要多少空间来存储信息需要在运行时动态请求内存空间。可以使用 new 操作符来定义一个变量使用 heap 方式再运行时动态分配内存空间,并返回分配的地址,例如:

new int;

以上定义一个使用 heap 方式分配一个足够存储 int 类型的内存空间,并返回这个地址。分配的地址可以存储在一个指针中,可以在后续中使用:

int *p = new int;
*p = 5;

以上请求了一个动态内存来存储一个 int 类型数据,然后在这个地址存储值为 5。注意指针 p 变量存储在 stack 静态内存中,且其值为 heap 动态内存分配的 int 型数据的地址,结果 5 是存储在 heap 动态内存中的。

本地静态内存 stack 下的变量,内存管理是自动进行的不需要手动干预,使用 heap 方式动态分配的内存空间需要手动干预操作,当不再需要动态内存空间的数据时使用 delete 操作符来释放空间。

示例:

int *p = new int; // request memory
*p = 5; // store value

cout << *p << endl; // use value

delete p; // free up the memory

忘记释放使用 new 请求的动态内存空间会导致内存泄漏问题,因为在程序关闭时才会释放空间。

注意 delete 操作的是指针类型。并且释放的只是 heap 动态内存,指针本身是存储在 stack 中的。当内存释放后,指针不指向某个内存地址,次时这个指针叫做:dangling pointers 悬浮指针。可以重新让这个指针指向内存地址:

int *p = new int; // request memory
*p = 5; // store value

delete p; // free up the memory
// now p is a dangling pointer

p = new int; // reuse for a new address

也可以定义一个 NULL 的空指针:

int *ptr = NULL;

也可以指向一个数组:

int *p = NULL; // Pointer initialized with null
p = new int[20]; // Request memory
delete [] p; // Delete array pointed to by p

注意释放内存时要加上方括号[]

动态内存分配很有用,如当程序需要读取一张图片时,你提前并不知道图片的大小,需要多大的内存空间来存储。这时候使用动态内存来定义就很方便了。

数据大小

虽然不同类型数据的内存占用大小和不同架构的设备有关或使用的编译器,但 c++ 有一个最小的数据大小定义:
123.png

可以使用 sizeof 指令来获取特定数据类型当前占用内存空间,如:

sizeof(int)

返回值为此类型数据的内存大小,单位为字节:

cout << "char: " << sizeof(char) << endl;
cout << "int: " << sizeof(int) << endl;
cout << "float: " << sizeof(float) << endl;
cout << "double: " << sizeof(double) << endl;
int var = 50;
cout << "var: " << sizeof(var) << endl;

/* Outputs
char: 1
int: 4
float: 4
double: 8
var: 4
*/ 

也可以获取数组的内存占用大小:

double myArr[10];
cout << sizeof(myArr) << endl; 

//Outputs 80

一个 double 占用 8 个字节,所以此数组共使用 80 个字节。

可以使用 sizeof 分别读取整个数组和单个元素的内存空间来计算数组元素个数:

int numbers[100];
cout << sizeof(numbers) / sizeof(numbers[0]);

// Outputs 100

以上就是 c++ 数据类型及指针的介绍。

]]>
<![CDATA[Speedtest CLI 命令行测速工具]]> https://blog.niekun.net/archives/1911.html 2020-11-16T13:08:00+08:00 2020-11-16T13:08:00+08:00 admin https://niekun.net speedtest 是国外很流行的测速平台,可以直接在其网站上测试本地上行下行带宽,最近看到其提供了本地命令行工具,使用起来更加方便了。

官网:https://www.speedtest.net/

安装

首先需要到官网安装对应平台的 CLI 工具:https://www.speedtest.net/apps/cli

Linux 类系统可以直接在使用 apt/brew 等包管理工具方便的安装,Windows 下需要手动下载 exe 可执行文件使用。

macOS:

brew tap teamookla/speedtest
brew update
brew install speedtest --force

ubuntu:

sudo apt-get install gnupg1 apt-transport-https dirmngr
export INSTALL_KEY=379CE192D401AB61
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $INSTALL_KEY
echo "deb https://ookla.bintray.com/debian generic main" | sudo tee  /etc/apt/sources.list.d/speedtest.list
sudo apt-get update
sudo apt-get install speedtest

windows:
首先在网站上下载可执行文件,点击下载 1.0.0 版

可执行文件为 speedtest.exe,需要将其目录加入系统 PATH 路径才可以直接在 terminal 调用,加入 PATH 方法参考我的教程:https://blog.niekun.net/archives/413.html

使用

在终端下使用 speedtest -h 命令查看帮助:

Speedtest by Ookla is the official command line client for testing the speed and performance of your internet connection.

Version: speedtest 1.0.0.2

Usage: speedtest [<options>]
  -h, --help                        Print usage information
  -V, --version                     Print version number
  -L, --servers                     List nearest servers
  -s, --server-id=#                 Specify a server from the server list using its id
  -I, --interface=ARG               Attempt to bind to the specified interface when connecting to servers
  -i, --ip=ARG                      Attempt to bind to the specified IP address when connecting to servers
  -o, --host=ARG                    Specify a server, from the server list, using its host's fully qualified domain name
  -p, --progress=yes|no             Enable or disable progress bar (Note: only available for 'human-readable'
                                    or 'json' and defaults to yes when interactive)
  -P, --precision=#                 Number of decimals to use (0-8, default=2)
  -f, --format=ARG                  Output format (see below for valid formats)
  -u, --unit[=ARG]                  Output unit for displaying speeds (Note: this is only applicable
                                    for ‘human-readable’ output format and the default unit is Mbps)
  -a                                Shortcut for [-u auto-decimal-bits]
  -A                                Shortcut for [-u auto-decimal-bytes]
  -b                                Shortcut for [-u auto-binary-bits]
  -B                                Shortcut for [-u auto-binary-bytes]
      --selection-details           Show server selection details
      --ca-certificate=ARG          CA Certificate bundle path
  -v                                Logging verbosity. Specify multiple times for higher verbosity
      --output-header               Show output header for CSV and TSV formats

 Valid output formats: human-readable (default), csv, tsv, json, jsonl, json-pretty

 Machine readable formats (csv, tsv, json, jsonl, json-pretty) use bytes as the unit of measure with max precision

 Valid units for [-u] flag:
   Decimal prefix, bits per second:  bps, kbps, Mbps, Gbps
   Decimal prefix, bytes per second: B/s, kB/s, MB/s, GB/s
   Binary prefix, bits per second:   kibps, Mibps, Gibps
   Binary prefix, bytes per second:  kiB/s, MiB/s, GiB/s
   Auto-scaled prefix: auto-binary-bits, auto-binary-bytes, auto-decimal-bits, auto-decimal-bytes

首先使用测试最近的测速节点 testspeed -L:

root@Marco-vostro-14-wsl:~# speedtest -L
Closest servers:

    ID  Name                           Location             Country
==============================================================================
 29105  陕西移动5G                 Xi'an                China
  2461  China Unicom                   Chengdu              China
  4575  China Mobile Group Sichuan     Chengdu              China
 24337  China Mobile Group Sichuan Co.,Ltd. Chengdu              China
 35527  sccn                           Chengdu              China
 31985  China Unicom                   Chongqing            China
 17584  Chongqing Mobile Company       Chongqing            CN
  5530  CCN                            Chongqing            China
 16145  Lanzhou,China Mobile,Gansu     Lanzhou              China

执行 speedtest 命令进行测速,默认使用上面测试的最近的节点:

root@Marco-vostro-14-wsl:~# speedtest

   Speedtest by Ookla

     Server: Chongqing Mobile Company - Chongqing (id = 17584)
        ISP: China Telecom
    Latency:    31.45 ms   (25.47 ms jitter)
   Download:    14.31 Mbps (data used: 17.5 MB)
     Upload:     8.23 Mbps (data used: 11.3 MB)
Packet Loss:     0.0%
 Result URL: https://www.speedtest.net/result/c/bee17d45-7b9c-44e6-8a6e-ef99d84ba441

以上就是简单的用法,可以查看帮助了解更多的使用方法。

]]>
<![CDATA[两个子网间网络互访的原理]]> https://blog.niekun.net/archives/1906.html 2020-11-13T11:29:00+08:00 2020-11-13T11:29:00+08:00 admin https://niekun.net 之前我介绍过子网,子网掩码,网关等基本概念解析,可以参考:https://blog.niekun.net/archives/1885.html

在不同子网下的设备是不能直接访问的,因为他们在不同的网段内,比如:

FbdOD.gif

  • 路由 A 下有子网 192.168.1.0/24
  • 路由 B 下有子网 192.168.3.0/24
  • 设备 james:192.168.1.10/24 网关:192.168.1.1
  • 设备 Johnny:192.168.3.10/24 网关:192.168.3.1
  • 两个路由的 wan 网口在同一网段:192.168.2.0/24 下

正常情况下路由 A 下的设备无法访问路由 B 下的设备,因为他们在不同的子网网段下。

路由 A 有两个网段:192.168.1.0 和 192.168.2.0,所以它可以同时看见这两个网段的设备,但是它并不知道 192.168.3.0 这一网段的存在。同样的道理,路由 B 并不知道 192.168.1.0 这一网段的存在。

当你给一个设备添加一个路由表 route table,就可以告诉它有其他网段可以进行访问,同时需要告诉它一个可以访问到这个新网段的网关,这样就可以访问到其他网段了。

例如让路由 A 访问到路由 B 的子网 192.168.3.0 可以通过给路由 A 添加新的路由表来实现,使用 ip route 命令:

ip route add 192.168.3.0/24 via 192.168.2.2

以上命令的意义是添加路由 B 子网的路由表 192.168.3.0/24,网关地址是路由 B 的 wan 地址 192.168.2.2。

因为路由 B 可以访问到两个网络 192.168.2.0 和 192.168.3.0,而路由 A 和路由 B 通过 192.168.2.0 互访,所以通过给路由 A 添加以上路由表然后网关地址为路由 B 和外部通信的地址,这样路由 A 就可以访问到路由 B 的子网了。

现在路由 A 可以访问到路由 B 的子网了,那么路由 A 下的设备如何访问路由 B 的子网设备呢?原理是相同的,可以通过给设备添加路由 B 子网的路由表,然后由于路由 A 已经可以访问 路由 B 子网了,所以将网关地址设置为路由 A 地址即可:

ip route add 192.168.3.0/24 via 192.168.1.1

在路由 A 下的 James 执行以上命令后,就可以访问到路由 B 下的 Johnny 了。

同样的原理,在路由 B 上设置路由表及网关就可以实现子网互访了。

参考链接:
understand the 'ip route' command for cisco routers

]]>