Marco Nie - c++ https://blog.niekun.net/category/cpp/ QT 中通过 QCustomPlot widget 绘制可视化曲线表 https://blog.niekun.net/archives/2208.html 2021-03-19T17:24:46+08:00 今天在项目中需要添加一个柱状图,但由于我们的项目是 QT 4.8 的所以不支持 QtCharts。查询了下发现有 QCustomPlot 可以完美的实现需求,使用方法也很简单。官网:https://www.qcustomplot.com/下载:https://www.qcustomplot.com/index.php/downloadQCustomPlot 只有两个文件 qcustomplot.cpp 和 qcustomplot.h,将其复制到项目目录中并添加到项目中。然后引用头文件即可:#include "qcustomplot.h" 我们需要在 ui 中添加一个 widget 然后右键点击控件,选择提升:提升的 class 名称修改为 QCustomPlot:点击 add 然后点击 promote 即可。编译后可以看到图表样式:在使用中如果需要根据数据变化刷新渲染的图形,记得在修改数据后调用 c++ 类型转换 https://blog.niekun.net/archives/2066.html 2021-01-14T11:58:00+08:00 Implicit conversion 隐式转换当一个数据复制为兼容格式的类型时,隐式转换可以自动完成。请看下面示例:double a = 100.1; int b; b = a; cout << b << endl; //output: //100以上示例中,我们将 double 类型的数据复制给 int 类型的变量,不会引起语法报错,这就是 Implicit 隐式类型转换,也叫 standard conversion 标准转换。标准转换对一些基本的数据类型有效,能够对一些 numerical 数值类型的数据间进行转换,如:double to int,int to float,short to double,int to bool 等,也可以在 pointer 指针类型间转换。从小一些的整型如 short 转换到 int 类型,或者从 float 类型转换到 double 类型,这种转换过程叫做 promotion 晋升操作。这种转换可以确保原始数据完整的复制到目标数据类型中。其他数学运算类型间的转换可能不一定会完整的保留原始数据,下面是几种情况举例:一个负整数转换为 unsigned 类型,结果为 unsigned 类型数据所能表达的最大值数据转换为 bool 类型的数据时,对于数值类型原始数据为 0 时,对于指针类型指针数据为 null pointer 时,对应转换为 false。原始数据为所有其他情况时,转换结果为 true。浮点型数据转换为整型数据,数据会被截取整数部分,如果数据结果超出目标数据类型所能表达的最大值,会得到 undefined我们可以看到,隐式转换可能会带来数据精度的丢失,编译器此时会提示一条 warning 警告。可以通过使用 explicit conversion 显式转换来避免警告信息。class 的隐式转换class 中的隐式转换通过以下三个 function 控制:单参数的 constructors 允许从一个特定类型的数据隐式转换构造为一个 object通过等号操作符 Assignment operator 复用来隐式转换类型传播符 Type-cast 来隐式转换为特定的类型下面示例解释各种方式的含义:class A {}; class B { public: B(A a) {} //constructor: conversion from A B operator=(A a) {return *this;} // assignment operator: conversion from A operator A() {return A();} // type-cast: conversion to A }; int main() { A a; // instance a B b = a; // constructor b from a B c(a); // constructor c from a b = a; // assignment a to b a = b; // convert b to A type and assignment to a return 0; }以上实例分别介绍了三种隐式转换的方式。构造器的一个传入参数为 A 类型数据,也就是等同于可以将 A 类型因素转换为 B 类型通过操作符复用将等号= 重新定义,等号右边的为 A 类型数据时,以A类型数据作为构造器参数返回 B 类型 object 指针当执行 B 类型数据赋值给 A 类型时,会通过类型传播符定义的 function 返回 A 类型 object构造器初始化 object时,当只有一个参数时,就相当于把传入数据转换为对应 object 类型了。上面示例中可以看到,有两种方法来构造 object:B b = a; // constructor b from a B c(a); // constructor c from a以上两种方式都是将 a 作为初始化参数构造 B 类型的 object。编写了等号操作符复用后,对某个指定类型的外部 object 进行等号操作时就会将其转换为当前 object 类型,而不会报错。通过类型传播符可以将 object 转换为其他指定类型的 object。关于操作符复用的语法参考我之前的教程:https://blog.niekun.net/archives/1920.htmlexplicit 关键词当调用一个 function 时,对于其传递参数 c++ 允许进行隐式转换,这在一些情况下会引起一些问题,因为我们并不是所有情况下都希望自动进行转换的。在以上示例中加入 function:void function(B b) {} 此 function 有一个 B 类型的参数,但是在实际调用中,由于 B 中定义了 A 的隐式转换相关模块,所以我们在这里可以将 A 类型数据作为传入数据:fun(a); 实际中我们可能并不需要这种转换,我们希望的是这里只能将 B 类型数据作为传入参数。通过关键词 explicit 来定义 constructor 可以实现这个需求,修改 B 的 constructor:explicit B(A a) {} 再次执行程序,会发现以下几个指令会报错:B b = a; fun(a);通过 关键词 explicit 定义 constructor 的 class 不能通过 assignment 赋值符来初始化 object,也不能对 function 的传入参数进行隐式转换。explicit conversion 显式转换c++ 是一种严格区分数据类型的语言,对于那些会影响数据本身的转换,需要进行 explicit conversion 显式转换也叫做 type-casting。在过去的语法中有两种常规的 type-casting 方式:double e = 10.11; int f = int(e); int g = (int)e;第一种叫做 function 样式,第二种叫做 c-like C语言模式。对于那些基础数据类型的数据,这种转换模式没有什么问题,但这种语法对于 class 和 pointer 类型数据也会不加判断的进行转换,从而导致运行时的 runtime error。为了控制这些在 class 间进行转换的过程,新版 c++ 提供了 4 种 casting operators 传播符来供不同场景下使用:dynamic_cast <new_type> (expression)reinterpret_cast <new_type> (expression)static_cast <new_type> (expression)const_cast <new_type> (expression)dynamic_castdynamic_cast 只能用于某个 class 或(void*) 的指针。他的作用是确保转换后的目标类型指针指向的是一个完整有效的 object,而不是空 object。例如,从 derived class 指针转换为 base class 指针。但是对于多态化的 polymorphic class(包含 virtual 元素的 class),当且仅当指向的 object 是一个完整有效的目标 object 类型,使用 dynamic_cast 就可以从 base class 指针转换为 derived class 指针,请看如下示例:class Base { virtual void dummy() {} }; class Derived: public Base {int a;}; int main() { try { Base *b1 = new Base; Base *b2 = new Derived; Derived *d; d = dynamic_cast<Derived*>(b1); if (d == 0) cout << "null pointer on first type cast" << endl; d = dynamic_cast<Derived*>(b2); if (d == 0) cout << "null pointer on second type cast" << endl; } catch(exception e) { cout << e.what() << endl; } return 0; } //output: //null pointer on first type cast以上示例中,我们建立了 Base class 和 Derived class,其中 Base 含有一个 virtual function。然后我们创建两个 Base 类型指针,两个指针分别预分配类型为 Base 和 Derived。在之前的 c++ 教程中提到过可以新建 base 类型的变量然后使用 derived 类型数据,需要了解的可以查看:https://blog.niekun.net/archives/1927.html。然后我们创建一个 Derived 类型指针,使用 dynamic_cast 分别将上面建立的两个 Base 类型指针转换为 Derived 类型。由于 b2 虽然是 Base 类型指针,但是我们预分配内存类型为 Derived 类型,所以它其实包含了 Derived object 所有属性。这样转换后的类型为完整的 Derived 类型指针。所以 d 指针不为空。而 b1 完全是 Base 类型指针,所以转换后的类型是不完整的 Derived 类型指针,所以赋值后结果为空。dynamic_cast 可以将任意指针转换为 void* 类型指针。static_caststatic_cast 可以转换任意相关联 class 类型的指针。不仅仅从 derived 到 base,也可以从 base 到 derived。不会判断是否转换到目标指针是完整的数据类型,所以完全由编程人员判断转换操作是否是安全的。相比于 dynamic_cast 有更大适用范围。以下示例语法不会报错:class Base {}; class Derived: public Base {}; Base * a = new Base; Derived * b = static_cast<Derived*>(a);b 指针会得到一个不完整的 Derived 类型数据,运行时可能报错。因此,使用 static_cast 不仅可以转换那些直接支持隐式转换的 class 指针,也可以在那些不支持转换的 class 间进行转换。我们测试在其他数据将进行转换:double a = 12.23; int b = static_cast<int>(a); cout << b << endl; //OUTPUT: //12以上,我们将 double 类型的数据转换为 int 类型,这种转换可以直接通过隐式转换完成,但是使用显式转换语法实现更加明确清晰。但前提是原类型和目标类型必须是 related 有关联的 object 类型。reinterpret_castreinterpret_cast 可以将任意类型指针转换为其他任意类型指针,甚至是完全没有关联的两个类型。转换的过程就是将源数据的二进制数据复制到新指针地址。相比于 static_cast 有更大适用范围。以下代码可以正常执行:class A { /* ... */ }; class B { /* ... */ }; A * a = new A; B * b = reinterpret_cast<B*>(a);此时 b 指针指向的是一个和 B 类型完全不相干的数据,此时访问 b 指向的数据是不安全的。const_castconst_cast 可以操作 const 类型的指针数据,例如当一个 function 需要非 const 类型传入数据时,可以通过 const_cast 进行转换。请看下面示例:void test(int a) { cout << a << endl; } const int a = 10; test(a);以上示例中,调用 test function 会报错,因为传入参数需要是非 const 类型的数据。修改以上代码:const int a = 10; int *b = const_cast<int*>(&a); test(*b); //output: //10通过 const_cast 将 const int 转换为 int,这样就可以在 function 中使用了。参考链接Type conversions c++ 字符串数组指针的研究 https://blog.niekun.net/archives/1968.html 2020-12-08T21:37:00+08:00 在实际使用中发现对字符串的运用是一个容易混乱的地方,尤其是使用指针指向一个字符串数组的时候。下面做一些简单分析。一个简单的测试: const char* test1 = "abc"; const string test2 = "abc"; cout << test1 << endl; cout << *test1 << endl; cout << test2 << endl; cout << sizeof (test1) << endl; cout << sizeof (test2) << endl;输出如下:abc a abc 8 24以前我的教程里提到过,字符串就相当于一个字符数组。指针会指向它的首个字符地址。test1 指针理论上存储着字符串的首地址。但我们可以看到直接输出 test1 会得到实际字符串内容,而不是首个字符地址。输出 *test 会得到正常的首字符内容。字符串指针使用 sizeof 得到这个指针所占用内存大小,而不是字符串内容的大小。同时我们可以发现,一个字符指针可以直接指向一个字符串,而不需要先定义一个字符串变量然后建立指针指向这个变量。这是因为一个字符串可以看做一个字符数组,同时它也是一个整体,字符指针可以直接定义指向它。下面测试 int 型数组: const int test3[] = {1, 3 ,5}; const int* test4 = test3; cout << test3 << endl; cout << test4 << endl; cout << test4[0] << endl; cout << *test4 << endl; cout << sizeof(test4) << endl; cout << sizeof(test4[0]) << endl;输出结果:0x7ffee8cbaa58 0x7ffee8cbaa58 1 1 8 4可以看到直接输出数组名称或指针名称得到的是数组所在地址。*test 和 test[0] 会得到数组第一位内容。test4 是一个指针,所以 sizeof 得到的是这个指针做占用的内存空间而不是数组本身占用空间。无法通过 sizeof 计算出数组个数。以上实验,我们先建立了一个 int 型数组变量,然后建立 int 型指针指向这个变量,如果直接在一行中建立一个指针指向一个数组会报错,这就和上面测试的字符指针不一样了。因为其他类型的数组不同于字符串,它的每个元素是独立的个体,所以无法使用一个指针直接指向他们全部。下面做最后一个测试:const char* test5[] = { "abc", "def", "ohg", "asdf" }; const string test6[] = { "abc", "def", "ohg", "asdf" }; int x = sizeof (test5)/sizeof(test5[0]); cout << test5 << endl; cout << *test5 << endl; cout << test5[0] << endl; cout << *test5[0] << endl; cout << x << endl; cout << sizeof(test5) << endl; cout << sizeof(test5[0]) << endl; cout << "*************\n"; x = sizeof (test6)/sizeof(test6[0]); cout << test6 << endl; cout << *test6 << endl; cout << test6[0] << endl; cout << x << endl; cout << sizeof(test6) << endl; cout << sizeof(test6[0]) << endl;输出结果如下:0x7ffee596ea40 abc abc a 4 32 8 ************* 0x7ffee596e9e0 abc abc 4 96 24由于字符串本身就是一个字符数组,所以一个字符串数组相当于一个二维的数组。定义字符串数组的指针,就是数组中每个字符串对应的指针的集合。所以这个数组指针本身不是字符串类型的,而它的每个指针元素都是字符串指针类型的。可以看到类似于第一组测试,非字符串的指针直接输出指针名称,所以 test5 得到字符串数组的地址。*test5 和 test5[0] 都表示指针数组第一个元素,也就是字符串类型的指针,根据第一组实验可以知道使用字符串指针名称输出本身字符串而不是地址,所以输出此指针可以直接得到字符串内容。*test5[0] 就是字符串指针的首字符地址内容,也就是得到第一个字符串第一个字符的内容。test5 指针数组使用 sizeof 得到的是这个指针数组总共占用的内存大小,也就是每个指针大小的总和。除以单个指针大小就可以得到这个数组指针的个数,也就是对应指向的数组的元素个数。test6 字符串数组使用 sizeof 得到的是这个字符串数组所有元素的占用内存大小,除以单个字符串大小就可以得到这个数组的元素个数。我们可以看到 test5 和 test6 使用 sizeof 都可以得到数组的元素个数,但他们的原理是完全不同的,一个是使用指针的内存大小,一个是使用数组本身的内存大小。总结:字符串的指针名称可以直接输出字符串内容而不是地址。其他指针类型指向的数据,如字符,数字,字符串数组,number 型数组等,指针名输出的都是数据地址。指针使用 sizeof 得到的是指针所占用的内存大小。可以使用 sizeof(*Pointer) 得到数据本身大小。字符串数组的指针使用 sizeof 得到指针数组的总大小,可以用来间接计算数组元素个数。 Macros 聚集 in c++ https://blog.niekun.net/archives/1956.html 2020-12-01T11:37:00+08:00 在 c++ 中,一个 Macro 就是一段代码的聚合。使用这个 macro 名称就代表着对应的代码段。有两种常见的 macro:object 形式,function 形式。可以定义任意有效的字符作为 macro 名称,甚至是 c 关键词。object 形式object 形式的 macro 就是简单的使用一个 identifier 代替代码片段。使用 #define 定义一个 macro:#define TESTINT 1024 void main() { int a = TESTINT; cout << a << endl; } //output: //1024也可以定义一个片段:#define NUMBERS 1, 2, 3 void main() { int x[] = { NUMBERS }; //int x[] = { 1, 2, 3 };这两句效果相同 }也可以多层定义:#define NUMBER1 1 #define NUMBER2 NUMBER1以上示例中 NUMBER2 等于 NUMBER1。function 形式可以定义 function 形式的 macro,需要在定义中 macro 名称后加上圆括号()。例如:#define lang_init() c_init()定义后就可以使用 lang_init() 来调用 c_init() 了,类似于 alias。object 类型的 macro 可以和 function 类型的 macro 同名,区别就是有没有圆括号:#define test 100 #define test() func() void main() { int a = test; test(); }以上就是对 macro 的简单介绍。参考链接:https://gcc.gnu.org/onlinedocs/cpp/Macros.html c++ 中 string 字符串的含义 https://blog.niekun.net/archives/1953.html 2020-11-25T11:52:00+08:00 有如下代码: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 的地址赋给指针。 c++ 中数组类型数据作为 function 参数时的注意事项 https://blog.niekun.net/archives/1946.html 2020-11-25T11:16:00+08:00 关于 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 //2function 中使用在指针类型的传入数据时需要传入一个指针,当定义为 & 时可以直接传入数据名称。两种方法都是 by reference 会影响到对应内存地址的数据内容。可参考教程:C++ 入门教程之四 -- Functions参考链接:https://stackoverflow.com/questions/4839626/element-count-of-an-array-in-chttps://www.tutorialspoint.com/cplusplus/cpp_pointer_to_an_array.htm C++ 入门教程之八 -- Templates - Exceptions - Files https://blog.niekun.net/archives/1940.html 2020-11-23T21:41:00+08:00 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 例外管理器是通过三个关键词:try,catch 和 throw 来建立的。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 iostream 和 fstream:#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 的第二参数可以用来定义文件打开模式,一下是支持的模式列表:以上的 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 循环。 C++ 入门教程之七 -- 继承和多态 https://blog.niekun.net/archives/1927.html 2020-11-21T22:31:00+08:00 inheritance 继承inheritance 继承是面向对象编程最重要的概念之一。继承允许我们基于已有的 class 创建新的 class。这能够极大的方便我们创建应用程序。一个 class 的属性被其他 class 继承,我们称这个 class 叫做 base class。一个从 base class 继承属性的 class 我们称其为 derived class。一个 derived class 获得所有 base class 的功能,且可以有自己独有的功能:继承的概念是 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 构造器和销毁器复用操作符 functionfriend 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 名称来执行不同的指令。 C++ 入门教程之六 -- Classes 实践/头文件/操作符复用 https://blog.niekun.net/archives/1920.html 2020-11-21T16:42:00+08:00 下面我们通过实际应用来介绍如果在真实环境下应用。我们使用 Code::Blocks IDE 来进行调试。这是一个免费的 c++ 开发环境,能够应用于绝大多数使用场景。官网:http://www.codeblocks.org/在下载界面下载对应系统版本的安装包:http://www.codeblocks.org/downloads/26最好安装带编译器的版本,这样就可以直接使用。新建项目安装好 codeblock 后新建一个项目:选择 console application:根据提示设置项目名称等,完成项目建立。然后添加一个 class:设置 class 名称,取消勾选 has destructor,勾选 headers and implementation file shall be in same folder:完成后项目结构如下: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_HMyClass.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_Hbirthday.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_Hpersion.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 + 加起来。以下列表中的操作符可以被复用:不能被复用的符号有:: | .* | . | ?:我们定义一个简单的 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 的基本使用方法。 C++ 入门教程之五 -- Classes and Objects https://blog.niekun.net/archives/1918.html 2020-11-20T11:32:00+08:00 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:class我们通过创建 class 来表示 object,一个 class 描述了一个 object 的形象,但是它并不是一个真正的 object,他只是一个对某种 object 结构的定义。一个 class 可以用来建立多个 object。例如一套设计图纸可以用来作为蓝图修建多栋楼房。一个 class 包含:identify,attributes,behavior程序中,一个对象的 type 就是 class 的 name 名称;Attributes 属性可以是 properties 或 data 数据;behavior 行为通常是一个 function。例如我们建立一个银行系统程序:name: BankAccountattributes: accountNumber, balance, dateOpenedbehavior: 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 外部访问这个属性。也可以是使用关键词 private 或 protected,下面做详细介绍。建立一个 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 构造器,来使用不同的参数。