22 - 3 - 2

指针

指针操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。
对于指针p,通过 p 和 p 访问指针 p 存储的地址值和它指向的内存地址存储的值。即p = 地址,p = 值
建议: 没有指针赋值时赋值为空指针Null。此时指针的值为0,内存0的地址是为操纵系统保留的。
注意:空指针不提倡使用NULL来表示,C++11标准后,用*nullptr
来表示空指针。

指针运算:

对于数组指针,指针运算++ —代表着数组的元素位置变化。
i = 0;int a[]={1,2,3}; int ptr;
ptr = a ; ptr == & a[i];
ptr ==a[i]; #其中,对于ptr++,相当于i++
对于指针指向数组,不能“&”:ptr = a(此时会取得整个数组的地址);只想元素,要“&”:prt = &a[i]
a等价于 &a[0],a+1 会将地址加 4 个字节;但 &a+1 就是将地址增加 10*4 个字节

指针比较:

<、>、== :ptr <= &a[2] //此处比较地址

指针、数组:

int a[] ={1,2,3}; a= 1
int
ptr[3]; 运算符的优先级中,* 小于 [],所以 ptr 先和 [] 结合成为数组,然后再和 int * 结合形成数组的元素类型是 int * 类型,得到一个叫一个数组的元素是指针,简称指针数组。数组的每个元素都是一个指向 int类型元素的指针。
int (ptr[3]);优先级顺序是 ** 小于 (),() 等于 []ptr 先和 [] 结合成为数组,然后再和 int * 结合形成数组的元素类型是 int * 类型,得到一个叫一个数组的元素是指针
int (ptr)[3];优先级顺序是 ** 小于 ()() 等于 []()[] 的优先级一样,但是结合顺序是从左到右,所以先是 () 里的 *ptr 结合成为一个指针,然后是 (*ptr)[] 相结合成为一个数组,最后叫一个指针 ptr 指向一个数组,简称数组指针。
int p[3]={1, 2, 3};int (pp)[3]={1,2 ,3};两种情况都不正确,
int a[]={1,2,3};
1:int指针类型数组只能存放int指针;int p[3]={&a[0], &a[1], &a[2]};为正确。
const char
names[4] = {“Zara Ali”,”Hina Ali”,”Nuha Ali”,”Sara Ali”,},
0<=i<4,则为每一个names[i]存储一个str,利用*(name[i]+j) 则可以输出i对应str中的第j个字符

  1. for (int i=0; i<MAX; i++) {
  2. cout<<" --- names[i] = "<< names[i] <<endl;
  3. cout<<" --- *names[i] = "<< *names[i] <<endl;
  4. cout<<endl;
  5. cout<<" --- (*names[i] + 1) = "<< (*names[i] +1) <<endl;
  6. cout<<" --- (char)(*names[i] + 1) = "<< (char)(*names[i] +1) <<endl;
  7. cout<<" ------------------------------------ "<<endl<<endl<<endl<<endl;
  8. system("pause");
  9. }
  1. --- names[i] = Hina Ali
  2. --- *names[i] = H
  3. --- (*names[i] + 1) = 73
  4. --- (char)(*names[i] + 1) = I

2:指向数组的指针只能是一个3长度Int数组值。 int (*pp)[3]=&a为正确

动态一维数组:

定义:type arrayname = new type size; // size可以是变量
访问:for(int i=0;i
(arrayname+i)=i; // 或是 arrayname[i];
cout<<*(arrayname+i)<<”\n”; }
释放:delete [] arrayname;

指针的指针:

C - 图1

  1. int main(){
  2. int var = 100;
  3. int *p = NULL;
  4. int **pp = NULL;
  5. //1.
  6. p = &var;
  7. pp = &p;
  8. cout << "var : " << var << endl; //var : 100
  9. cout << "&var: " << &var << endl; //&var: 0x61fdfc
  10. cout << "p : " << p << endl; //p : 0x61fdfc
  11. cout << "*p : " << *p << endl; //*p : 100
  12. cout << "&p : " << &p << endl; //&p : 0x61fe00
  13. cout << "pp : " << pp << endl; //pp : 0x61fe00
  14. cout << "*pp : " << *pp << endl; //*pp : 0x61fdfc
  15. cout << "**pp: " << **pp << endl; //**pp: 100
  16. cout << "&pp : " << &pp << endl; //&pp : 0x61fe08
  17. //**pp = *p = var;*pp = *p = &var;pp = &p
  18. system("pause");
  19. return 0;
  20. }

ptr = &var
ptr = (&var) = var
pptr = &ptr
pptr = (&ptr) = ptr = &var
*pptr = (pptr) = (&var) = var

指针、函数:

参数
对于int arr1[3]={1,2,3},传入func(int arr2),有arr2 === arr1 。对arr2进行操纵,arr1也一样改变。本质是只改变数据,地址的没有改变。对arr2=temp,arr2的地址改变成了temp的地址,此时操作是修改temp。
sizeof(arr) / sizeof(arr[0])在func中会出现问题。如果把数组作为参数传入函数,那么这个数组会退化为一个指针 int
arr,所以最好是传入数组长度。
C++ 不支持在函数外返回局部变量的地址
函数
从函数返回指针需要创建指针函数,如int *ptrFun(){static int i[10]; return i; };

引用

对于int i = 10;

  1. int& r = i; r 和i 一致
  2. int r = i; r和i的地址不同
    • 不存在空引用。引用必须连接到一块合法的内存。
    • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
    • 引用必须在创建时被初始化。指针可以在任何时间被初始化。

引用、函数:

  1. void swap(int& x, int& y);
  2. int main ()
  3. {
  4. int a = 100;
  5. int b = 200;
  6. cout << "交换前,a 的值:" << a << endl;
  7. cout << "交换前,b 的值:" << b << endl;
  8. swap(a, b);
  9. cout << "交换后,a 的值:" << a << endl;
  10. cout << "交换后,b 的值:" << b << endl;
  11. return 0;
  12. }
  13. void swap(int& x, int& y)
  14. {
  15. int temp;
  16. temp = x; /* 保存地址 x 的值 */
  17. x = y; /* 把 y 赋值给 x */
  18. y = temp; /* 把 x 赋值给 y */
  19. return;
  20. }
  1. int &changevalue();
  2. int main()
  3. {
  4. int &a_return=changevalue();
  5. a_return =20;
  6. cout<<changevalue()<<endl;
  7. system("pause");
  8. }
  9. int &changevalue()
  10. {
  11. static int a_return =-29;
  12. return a_return;
  13. }
  • 以引用返回函数值,定义函数时需要在函数名前加 &
  • 用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。

  • 被引用的对象不能超出作用域,不能返回局部变量的引用。局部函数销毁后会造成空引用。

  • 不能返回函数内部new分配的内存的引用。如果函数返回的变量是临时变量而无实际值,此引用空间将无法释放。
  • 可以返回类成员的引用,但最好是const。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。

    结构

    1. struct type_name {
    2. member_type1 member_name1;
    3. member_type2 member_name2;
    4. member_type3 member_name3;
    5. .
    6. .
    7. } object_names;

    成员访问运算符(.)

    1. void func( struct Books book )
    2. {
    3. cout << "书标题 : " << book.title <<endl;
    4. cout << "书作者 : " << book.author <<endl;
    5. cout << "书类目 : " << book.subject <<endl;
    6. cout << "书 ID : " << book.book_id <<endl;
    7. }

    指向结构的指针

    指向结构的指针:struct Books *struct_pointer;
    针变量中存储结构变量的地址:struct_pointer = &Book1;
    使用指向该结构的指针访问结构的成员:struct_pointer->title;

    typedef 关键字

    1. typedef struct Books
    2. {
    3. char title[50];
    4. char author[50];
    5. char subject[100];
    6. int book_id;
    7. }Books;

    之后创建变量可以直接使用Books book1,book2;此时不需要使用struct关键字。
    typedef也可以用来定义非结构类型:typedef short *shortint; short a,b,c; 此时abc都是short类型。

    构造函数

  • 1、与类同名的函数是构造函数。

    • 主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,一般不作初始化以外的事情;
    • 程序运行时,当对象被创建后,该对象所属的类的构造函数自动被调用,在该对象生存期中也只调用这一次;
    • 可以重载。class1;class1(int i);此时创建类为Class1 class1或Class1 class1(1)。无法使用class()来创建。
    • 访问属性应该是public。
    • 没有编写构造函数,则 C++ 会自动提供一个。

      析构函数

  • 2、~ 类名的是类的析构函数。

    • 当对象被销毁时,会自动调用析构函数。
    • 构函数没有返回类型。
    • 析构函数不能接收实参,因此它们从不具有形参列表。
    • 由于析构函数不能接收实参,因此只能有一个析构函数。

      1. class class1
      2. {
      3. //private/public/protected:
      4. // Data variables;
      5. // Member functions(){};
      6. public:
      7. class1(){lenth = 0};
      8. ~class1(){cout<<"~class1"<<endl;}
      9. int length;
      10. int getLen();
      11. setLen(int len){//自动内联
      12. length = len;
      13. return;
      14. }
      15. outLen();
      16. };
      17. int class1::getLen() { return length; };//外联
      18. //inline建立
      19. inline void class1::outLen()
      20. {
      21. cout << "length:" << length << endl;
      22. return;
      23. };
      24. int main()
      25. {
      26. class1 c1;
      27. c1.length = 50;
      28. int len;
      29. len = c1.getLen();
      30. cout<<len<<endl;
      31. return 0;
      32. }

      public:公共成员在类的外部是可访问的。而私有的成员和受保护的成员不能使用直接成员访问运算符 (.)
      使用范围解析运算符 ::在外部定义函数。

    • :: 叫作用域区分符,指明一个函数属于哪个类或一个数据属于哪个类。

    • :: 可以不跟类名,表示全局数据或全局函数(即非成员函数)。

      拷贝构造函数

      当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。
      在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。
  • 通过使用另一个同类型的对象来初始化新创建的对象。

  • 复制对象把它作为参数传递给函数。
  • 复制对象,并从函数返回这个对象。
  • 在类中没有定义拷贝构造函数,编译器会自行定义一个。
  • 如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。

    1. classname (const classname &obj)
    2. {
    3. // 构造函数的主体
    4. }//obj 是一个对象引用,该对象是用于初始化另一个对象的。

    类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:

    • (1)一个对象以值传递的方式传入函数体
    • (2)一个对象以值传递的方式从函数返回
    • (3)一个对象需要通过另外一个对象进行初始化。
  • 只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义拷贝构造函数也可以拷贝;
  • 有的类有一个数据成员是指针,或者是有成员表示在构造函数中分配的其他资源,这两种情况下都必须定义拷贝构造函数。

    友元函数

    定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。
    友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
    友元可以是一个函数、类,即友元函数、友元类。友元类整个类及其所有成员都是友元。如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend。

    1. class class1
    2. {
    3. friend void printWidth();
    4. friend class Class2;
    5. }
    6. class Class2{};

    友元函数没有this指针,参数要有三种情况:

  • 要访问非static成员时,需要对象做参数;

  • 要访问static成员或全局变量时,则不需要对象做参数;
  • 如果做参数的对象是全局对象,则不需要对象做参数.
  • 可以直接调用友元函数,不需要通过对象或指针

    1. class INTEGER
    2. {
    3. friend void Print(const INTEGER& obj);//声明友元函数
    4. };
    5. void Print(const INTEGER& obj
    6. {
    7. //函数体
    8. }
    9. void main()
    10. {
    11. INTEGER obj;
    12. Print(obj);//直接调用
    13. }

    内联函数

    引入内联函数的目的是为了解决程序中函数调用的效率问题。程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的节省。所以内联函数一般都是1-5行的小函数。在使用内联函数时要留神:

  • 1.在内联函数内不允许使用循环语句和开关语句;

  • 2.内联函数的定义必须出现在内联函数第一次调用之前;
  • 3.类结构中所在的类说明内部定义的函数是内联函数。

    this指针

    每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。
    -友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。
    -this 的目的总是指向“这个”对象,所以 this 是一个常量指针,不允许改变 this 中保存的地址。 ```cpp class Box{ private:

    1. double length; // Length of a box
    2. double breadth; // Breadth of a box
    3. double height; // Height of a box

    public:

    1. Box(double l=2.0, double b=2.0, double h=2.0)
    2. {
    3. length = l;
    4. breadth = b;
    5. height = h;
    6. }
    7. double Volume()
    8. {
    9. return length * breadth * height;
    10. }
    11. int compare(Box box)
    12. {
    13. return this->Volume() > box.Volume();
    14. }
    15. Box* get_address() //得到this的地址
    16. {
    17. return this;
    18. }

    } int main(void) { Box Box1(3.3, 1.2, 1.5); // Declare box1 Box Box2(8.5, 6.0, 2.0); // Declare box2 Box *ptrBox;
    if(Box1.compare(Box2)) {

    1. cout << "Box2 is smaller than Box1" <<endl;

    } else {

    1. cout << "Box2 is equal to or larger than Box1" <<endl;

    } Box p = box1.get_address(); // this 指针的类型可理解为 Box。 p = box2.get_address(); // 此时得到两个地址分别为 box1 和 box2 对象的地址。

    //保存第一个对象的地址 ptrBox = &Box1; // 尝试使用成员访问运算符来访问成员 cout << “Volume of Box1: “ << ptrBox->Volume() << endl; return 0; }

  1. 访问指向类的指针的成员,需要使用成员访问运算符 **->**,与访问指向结构的指针一样。在使用指针之前,对指针进行初始化。
  2. <a name="i0Yqu"></a>
  3. #### 静态类成员
  4. **static** 关键字来把类成员定义为静态的,此时无论创建多少个类对象,都只有一个静态成员副本。<br />静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。<br /> 静态成员变量在类中仅仅是声明,没有定义,所以要在类的外面定义,实际上是给静态成员变量分配内存。不能把静态成员的初始化放置在类的定义中,可以在类的外部通过使用范围解析运算符 **::** 来重新声明静态变量从而对它进行初始化。如果不加定义就会报错,初始化是赋一个初始值,而定义是分配内存。<br />// 初始化类 Box 的静态成员 <br />int Box :: object = 0;
  5. <a name="bbgDW"></a>
  6. ##### 静态成员函数
  7. 函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 **::** 就可以访问。<br />静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。<br />静态成员函数有一个类范围,他们不能访问类的 this 指针。您可以使用静态成员函数来判断类的某些对象是否已被创建。
  8. - 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
  9. <a name="zLii5"></a>
  10. ### 继承
  11. 指定新建的类继承了一个已有的类的成员即可。这个已有的类称为**基类**,新建的类称为**派生类**。
  12. | 访问 | public | protected | private |
  13. | --- | --- | --- | --- |
  14. | 同一个类 | yes | yes | yes |
  15. | 派生类 | yes | yes | no |
  16. | 外部的类 | yes | no | no |
  17. 一个派生类继承了所有的基类方法,但下列情况除外:
  18. - 基类的构造函数、析构函数和拷贝构造函数。
  19. - 基类的重载运算符。
  20. - 基类的友元函数。
  21. 几乎不使用 **protected** **private** 继承,通常使用 **public** 继承。当使用不同类型的继承时,遵循以下几个规则:
  22. - **公有继承(public):**当一个类派生自**公有**基类时,基类的**公有**成员也是派生类的**公有**成员,基类的**保护**成员也是派生类的**保护**成员,基类的**私有**成员不能直接被派生类访问,但是可以通过调用基类的**公有**和**保护**成员来访问。
  23. - 基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private
  24. - **保护继承(protected):** 当一个类派生自**保护**基类时,基类的**公有**和**保护**成员将成为派生类的**保护**成员。
  25. - 基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private
  26. - **私有继承(private):**当一个类派生自**私有**基类时,基类的**公有**和**保护**成员将成为派生类的**私有**成员。
  27. - 基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private
  28. ```cpp
  29. #include<iostream>
  30. using namespace std;
  31. class Animal{
  32. public:
  33. string name;
  34. eat() { cout << "Animal eat..." << endl;};
  35. sleep(){ cout << "Animal sleep..." << endl;};;
  36. };
  37. class Dog : public Animal{
  38. public:
  39. int feet;
  40. Dog(){feet = 4};
  41. };
  42. class Age{
  43. public:
  44. int age;
  45. Age(){age = 0;};
  46. void outAge() { cout << "Age:" << age << endl}
  47. };
  48. class Cat : public Dog , public Age{
  49. };
  50. int main(){
  51. Cat cat;
  52. cat.eat();
  53. cat.outAge();
  54. system("pause");
  55. return 1;
  56. };

虚继承

—(在创建对象的时候会创建一个虚表)在创建父类对象的时候
多继承(环状继承),A->D, B->D, C->(A,B)会使D创建两个对象,要解决上面问题就要用虚拟继承格式
格式:class 类名: virtual 继承方式 父类名

  1. class D{......};
  2. class B: virtual public D{......};
  3. class A: virtual public D{......};
  4. class C: public B, public A{.....};

重载

函数重载

声明几个功能类似的同名函数,这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。

  1. void happy(int i){};
  2. void happy(double i){};

运算符重载

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

  1. // 重载负运算符( - ) ----一元重载
  2. Distance operator- ()
  3. {
  4. feet = -feet;
  5. inches = -inches;
  6. return Distance(feet, inches);
  7. }
  8. // 重载 + 运算符,用于把两个 Box 对象相加 ----二元重载
  9. Box operator+(const Box& b)
  10. {
  11. Box box;
  12. box.length = this->length + b.length;
  13. box.breadth = this->breadth + b.breadth;
  14. box.height = this->height + b.height;
  15. return box;
  16. }
  17. // 主程序中使用
  18. // Box3 = Box1 + Box2; // 把两个对象相加,得到 Box3

不可重载的运算符列表:

  • .:成员访问运算符
  • .*, ->*:成员指针访问运算符
  • :::域运算符
  • sizeof:长度运算符
  • ?::条件运算符
  • #: 预处理符号

重载自增(++)自减(—)运算符:operator++(int)的括号内int指的是后缀,而非整型。对于++i,operator++()即可。
函数调用运算符 () 可以被重载用于类的对象。当重载 () 时,您不是创造了一种新的调用函数的方式,相反地,这是创建一个可以传递任意数目参数的运算符函数。
类成员访问运算符( -> )重载,必须是一个成员函数。如果使用了 -> 运算符,返回类型必须是指针或者是类的对象。

  1. class Ptr{
  2. //...
  3. X * operator->();
  4. };

22 - 3 - 3

多态

多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
在基类中的函数一开始被固定为基类版本,在程序执行前就准备好,这是静态多态或静态链接、早绑定。
在函数声明前加入virtual关键字来实现每个子类独立实现该函数。
形成多态必须具备三个条件:

  1. 必须存在继承关系;
  2. 继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字Virtual声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);
  3. 存在基类类型的指针或者引用,通过该指针或引用调用虚函数;

    虚函数

    虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
    是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。virtual int func(){};
    我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定
  • 虚函数必须要有实现
  • 在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
  • 友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
  • 析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。

    纯虚函数

    基类中不对虚函数给出有意义的实现,这个时候就会用到纯虚函数。virtual int func()=0;
    用final函数来避免父类函数被重写。

    数据抽象

    只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。
    一种依赖于接口和实现分离的编程(设计)技术。
    重要的优势:

  • 类的内部受到保护,不会因无意的用户级错误导致对象状态受损。

  • 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。

如果只在类的私有部分定义数据成员,编写该类的作者就可以随意更改数据。如果实现发生改变,则只需要检查类的代码,看看这个改变会导致哪些影响。如果数据是公有的,则任何直接访问旧表示形式的数据成员的函数都可能受到影响。

访问标签强制抽象

使用访问标签来定义类的抽象接口。一个类可以包含零个或多个访问标签:

  • 用公共标签定义的成员都可以访问该程序的所有部分。一个类型的数据抽象视图是由它的公共成员来定义的。
  • 使用私有标签定义的成员无法访问到使用类的代码。私有部分对使用类型的代码隐藏了实现细节。

    设计策略

    抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。
    在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。

    数据封装

    将某些东西包装盒隐藏起来,让外界无法直接使用,只能通过某些特定的方式才能访问
    封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念避免受到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的 OOP 概念,即数据隐藏
    数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
    把一个类定义为另一个类的友元类,会暴露实现细节,从而降低了封装性。理想的做法是尽可能地对外隐藏每个类的实现细节。

    设计策略

    通常情况下,我们都会设置类成员状态为私有(private),除非我们真的需要将其暴露,这样才能保证良好的封装性
    通常应用于数据成员,但它同样适用于所有成员,包括虚函数。

虚函数可以为private, 并且可以被子类覆盖(因为虚函数表的传递),但子类不能调用父类的private虚函数。虚函数的重载性和它声明的权限无关。
一个成员函数被定义为private属性,标志着其只能被当前类的其他成员函数(或友元函数)所访问。而virtual修饰符则强调父类的成员函数可以在子类中被重写,因为重写之时并没有与父类发生任何的调用关系,故而重写是被允许的。
编译器不检查虚函数的各类属性。被virtual修饰的成员函数,不论他们是private、protect或是public的,都会被统一的放置到虚函数表中。对父类进行派生时,子类会继承到拥有相同偏移地址的虚函数表(相同偏移地址指,各虚函数相对于VPTR指针的偏移),则子类就会被允许对这些虚函数进行重载。且重载时可以给重载函数定义新的属性,例如public,其只标志着该重载函数在该子类中的访问属性为public,和父类的private属性没有任何关系。
纯虚函数可以设计成私有的,不过这样不允许在本类之外的非友元函数中直接调用它,子类中只有覆盖这种纯虚函数的义务,却没有调用它的权利。

接口-抽象类

接口描述了类的行为和功能,而不需要完成类的特定实现。
C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。
如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的
设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。
如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。
可用于实例化对象的类被称为具体类

文件流

C++ 中另一个标准库 fstream

数据类型 描述
ofstream 该数据类型表示输出文件流,用于创建文件并向文件写入信息。
ifstream 该数据类型表示输入文件流,用于从文件读取信息。
fstream 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。

打开文件

ofstreamfstream 对象都可以用来打开文件进行写操作,如果只需要打开文件进行读操作,则使用 ifstream 对象。
void open(const char filename, ios::openmode mode);
*open()
成员函数的第一参数指定要打开的文件的名称和位置,第二个参数定义文件被打开的模式。

模式标志 描述
ios::app 追加模式。所有写入都追加到文件末尾。
ios::ate 文件打开后定位到文件末尾。
ios::in 打开文件用于读取。
ios::out 打开文件用于写入。
ios::trunc 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。

以写入模式打开文件,并希望截断文件,以防文件已存在:
ofstream outfile; outfile.open(“file.dat”, ios::out | ios::trunc );
打开一个文件用于读写:
ifstream afile; afile.open(“file.dat”, ios::out | ios::in );

关闭文件

C++ 程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。close()函数是iofstream对象的一个成员

读写文件

ofstreamfstream 对象 -流插入运算符( << )向文件写入信息:
outfile << data << endl;
ifstreamfstream 对象 -流提取运算符( >> )从文件读取信息
infile >> data;

文件位置指针

istreamostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istream 的 seekg(”seek get”)和关于 ostream 的 seekp(”seek put”)。
seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。

  1. // 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
  2. fileObject.seekg( n );
  3. // 把文件的读指针从 fileObject 当前位置向后移 n 个字节
  4. fileObject.seekg( n, ios::cur );
  5. // 把文件的读指针从 fileObject 末尾往回移 n 个字节
  6. fileObject.seekg( n, ios::end );
  7. // 定位到 fileObject 的末尾
  8. fileObject.seekg( 0, ios::end );
  1. char data[100];
  2. ifstream infile;
  3. ofstream outfile;
  4. infile.open("test.txt");
  5. outfile.open("test_1.txt");
  6. while (!infile.eof())
  7. {
  8. infile >> data;
  9. //fstream读取txt文件时发现末尾一行会被读取两次,
  10. //可能是因为到达文件末尾时,eof仍然是false,只有继续往下读时才会变成true。
  11. if(infile.eof())
  12. break;
  13. cout << data << endl;
  14. outfile << data << endl;
  15. }

异常处理

try、catch、throw

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。 ```cpp struct MyException : public exception{ const char * what () const throw ()
    1. {
    2. return "C++ Exception";
    3. }
    };

int main(){ try { int a = 4; if(a < 3) { throw MyException(); } else if(a == 4) { throw exception(); }; return 0; }catch(MyException& e) { cout << “MyException caught” << endl; cout << e.what() << endl; }catch(exception& e) { cout << “exception caught” << endl; }

};

  1. **const throw()** 不是函数,这个东西叫异常规格说明,表示 **what** 函数可以抛出异常的类型,类型说明放到 **()** 里,这里面没有类型,就是声明这个函数不抛出异常,通常函数不写后面的就表示函数可以抛出任何类型的异常。
  2. 在函数的声明中列出这个函数可能抛掷的所有异常类型。void fun() throw(ABCD);<br />部使用异常接口可以抛出任何异常。 void mightThrow();<br />不可以抛出任何异常使用关键字 noexcept void new_style() noexcept;或void old_stytle() throw();
  3. <a name="X0Bxd"></a>
  4. #### c++标准异常
  5. | 异常 | 描述 |
  6. | --- | --- |
  7. | **std::exception** | 该异常是所有标准 C++ 异常的父类。 |
  8. | std::bad_alloc | 该异常可以通过 **new** 抛出。 |
  9. | std::bad_cast | 该异常可以通过 **dynamic_cast** 抛出。 |
  10. | std::bad_exception | 这在处理 C++ 程序中无法预期的异常时非常有用。 |
  11. | std::bad_typeid | 该异常可以通过 **typeid** 抛出。 |
  12. | **std::logic_error** | 理论上可以通过读取代码来检测到的异常。 |
  13. | std::domain_error | 当使用了一个无效的数学域时,会抛出该异常。 |
  14. | std::invalid_argument | 当使用了无效的参数时,会抛出该异常。 |
  15. | std::length_error | 当创建了太长的 std::string 时,会抛出该异常。 |
  16. | std::out_of_range | 该异常可以通过方法抛出,例如 std::vector std::bitset<>::operator[]()。 |
  17. | **std::runtime_error** | 理论上不可以通过读取代码来检测到的异常。 |
  18. | std::overflow_error | 当发生数学上溢时,会抛出该异常。 |
  19. | std::range_error | 当尝试存储超出范围的值时,会抛出该异常。 |
  20. | std::underflow_error | 当发生数学下溢时,会抛出该异常。 |
  21. <a name="E9B0a"></a>
  22. ### 动态内存
  23. 内存分为两个部分:
  24. - **栈:**在函数内部声明的所有变量都将占用栈内存。
  25. - **堆:**这是程序中未使用的内存,在程序运行时可用于动态分配内存。
  26. **new** 运算符为给定类型的变量在运行时分配堆内的内存,并返回所分配的空间地址。new data-type;
  27. ```cpp
  28. double* pvalue = NULL; // 初始化为 null 的指针
  29. pvalue = new double; // 为变量请求内存

自由存储区已被用完,可能无法成功分配内存。建议检查 new 运算符是否返回 NULL 指针。

  1. double* pvalue = NULL;
  2. if( !(pvalue = new double ))
  3. {
  4. cout << "Error: out of memory." <<endl;
  5. exit(1);
  6. }
  7. delete pvalue; // 释放 pvalue 所指向的内存

delete 操作符释放动态分配内存的变量所占用的内存。

多维数组

  1. // 动态分配,数组长度为 m
  2. int *array=new int [m];
  3. //释放内存
  4. delete [] array;

二维以上数组需要嵌套循环。注意维数+1,指针*+1(

  1. int **array
  2. // 假定数组第一维长度为 m, 第二维长度为 n
  3. // 动态分配空间
  4. array = new int *[m];
  5. for( int i=0; i<m; i++ )
  6. {
  7. array[i] = new int [n] ;
  8. }
  9. //释放
  10. for( int i=0; i<m; i++ )
  11. {
  12. delete [] array[i];
  13. }
  14. delete [] array;
  1. int ***array;
  2. // 假定数组第一维为 m, 第二维为 n, 第三维为h
  3. // 动态分配空间
  4. array = new int **[m];
  5. for( int i=0; i<m; i++ )
  6. {
  7. array[i] = new int *[n];
  8. for( int j=0; j<n; j++ )
  9. {
  10. array[i][j] = new int [h];
  11. }
  12. }
  13. //释放
  14. for( int i=0; i<m; i++ )
  15. {
  16. for( int j=0; j<n; j++ )
  17. {
  18. delete[] array[i][j];
  19. }
  20. delete[] array[i];
  21. }
  22. delete[] array;

delete与delete[]

针对简单类型 使用 new 分配后的不管是数组还是非数组形式内存空间用两种方式均可 如:

  1. int *a = new int[10];
  2. delete a;
  3. delete [] a;

分配简单类型内存时,内存大小已经确定,系统可以记忆并且进行管理,在析构时,系统并不会调用析构函数, 它直接通过指针可以获取实际分配的内存空间,哪怕是一个数组内存空间(在分配过程中 系统会记录分配内存的大小等信息,此信息保存在结构体_CrtMemBlockHeader中, 具体情况可参看VC安装目录下CRT\SRC\DBGDEL.cpp)
针对类Class,两种方式体现出具体差异:

  1. class A
  2. {
  3. private:
  4. char *m_cBuffer;
  5. int m_nLen;
  6. public:
  7. A(){ m_cBuffer = new char[m_nLen]; }
  8. ~A() { delete [] m_cBuffer; }
  9. };
  10. A *a = new A[10];
  11. // 仅释放了a指针指向的全部内存空间 但是只调用了a[0]对象的析构函数
  12. //剩下的从a[1]到a[9]这9个用户自行分配的m_cBuffer对应内存空间将不能释放 从而造成内存泄漏
  13. delete a;
  14. // 调用使用类对象的析构函数释放用户自己分配内存空间并且 释放了a指针指向的全部内存空间
  15. delete [] a;

如果ptr代表一个用new申请的内存返回的内存空间地址,即所谓的指针,那么:

  • delete ptr — 代表用来释放内存,且只用来释放ptr指向的内存。
  • delete[] rg — 用来释放rg指向的内存,还逐一调用数组中每个对象的 destructor。

对于像 int/char/long/int*/struct 等等简单数据类型,由于对象没有 destructor,所以用 delete 和 delete [] 是一样的!但是如果是C++ 对象数组就不同了!

命名空间

作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。
定义使用关键字 namespace namespace namespace_name{// 代码声明}
用带有命名空间的函数或变量,需要在前面加上命名空间的名称 name::code; // code 可以是变量或函数
一个命名空间的各个组成部分可以分散在多个文件中。所以,如果命名空间中的某个组成部分需要请求定义在另一个文件中的名称,则仍然需要声明该名称。

  1. namespace ns1//也可以是为已有的命名空间增加新的元素
  2. {
  3. void func()
  4. {
  5. std::cout << "namespace ns1 func" << std::endl;
  6. }
  7. }
  8. namespace ns2
  9. {
  10. void func()
  11. {
  12. std::cout << "namespace ns2 func" << std::endl;
  13. }
  14. }
  15. int main()
  16. {
  17. ns1::func();
  18. ns2::func();
  19. return 0;
  20. }
  1. **using namespace** 指令会告诉编译器,后续的代码将使用指定的命名空间中的名称,从而不用使用name::code,直接code就可以。<br />using 指令也可以用来指定命名空间中的特定项目。例如,如果您只打算使用 std 命名空间中的 cout 部分,using std::cout;使用cout是不需要再加上std,但是对endlcin等其他内容,很还是需要的。<br />**using** 指令引入的名称遵循正常的范围规则。名称从使用 **using** 指令开始是可见的,直到该范围结束。此时,在范围以外定义的同名实体是隐藏的。<br />嵌套命名空间:
  1. namespace namespace_name1 {
  2. // 代码声明
  3. namespace namespace_name2 {
  4. // 代码声明
  5. }
  6. }
  7. // 访问 namespace_name2 中的成员
  8. using namespace namespace_name1::namespace_name2;
  9. // 访问 namespace:name1 中的成员
  10. //使用的是 namespace_name1,那么在该范围内 namespace_name2 中的元素也是可用的
  11. using namespace namespace_name1;

命名空间及全局变量:

  1. #include <iostream>
  2. using namespace std;
  3. namespace A
  4. {
  5. int a = 100;
  6. namespace B //嵌套一个命名空间B
  7. {
  8. int a =20;
  9. }
  10. }
  11. int a = 200;//定义一个全局变量
  12. int main(int argc, char *argv[])
  13. {
  14. cout <<"A::a ="<< A::a << endl; //100 A-a
  15. cout <<"A::B::a ="<<A::B::a << endl; //20 B-a
  16. cout <<"a ="<<a << endl; //200 全局a
  17. cout <<"::a ="<<::a << endl; //200 全局a
  18. using namespace A;
  19. cout << "a =" << a << endl;// Reference to 'a' is ambiguous // 命名空间冲突,编译期错误
  20. int a = 30;
  21. cout <<"a ="<<a << endl; //30 局部a
  22. cout <<"::a ="<<::a << endl; //200 全局a
  23. //全局变量 a 表达为 ::a,用于当有同名的局部变量时来区别两者。
  24. using namespace A;
  25. cout << "a =" << a << endl; // a = 30 // 当有本地同名变量后,优先使用本地,冲突解除
  26. cout << "::a =" << ::a << endl; //::a = 200
  27. return 0;
  28. }

c++模板

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。
每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector vector

函数模板

  1. template <typename type> ret-type func-name(parameter list)
  2. {
  3. // 函数的主体
  4. }
  1. template <typename T>
  2. inline T const& Max (T const& a, T const& b)
  3. {
  4. return a < b ? b:a;
  5. }

类模板

  1. template <class type> class class-name {
  2. .
  3. .
  4. .
  5. }
  1. #include <iostream>
  2. #include <vector>
  3. #include <cstdlib>
  4. #include <string>
  5. #include <stdexcept>
  6. using namespace std;
  7. template <class T> //类模板就
  8. class Stack
  9. {
  10. private:
  11. vector<T> m_elems; //元素
  12. public:
  13. void push(T const&); //入栈 -- 引入指针引用
  14. void pop(); //出栈
  15. T top() const; //返回顶值
  16. bool empty() const //判空
  17. {
  18. return m_elems.empty();
  19. }
  20. };
  21. template <class T>
  22. void Stack<T>::push(T const& elem)
  23. { //元素压入栈
  24. m_elems.push_back(elem);
  25. }
  26. template <class T>
  27. void Stack<T>::pop()
  28. { //判空
  29. if(empty())
  30. {
  31. throw out_of_range("Stack<> :: pop(): empty stack ");
  32. }
  33. //尾元素弹出栈
  34. m_elems.pop_back();
  35. }
  36. template <class T>
  37. T Stack<T>::top() const
  38. {
  39. if(empty())
  40. { //判空
  41. throw out_of_range("Stack<> :: pop(): empty stack ");
  42. }
  43. return m_elems.back();
  44. };
  45. int main()
  46. {
  47. try{
  48. //int栈
  49. Stack<int> iStack;
  50. //string栈
  51. Stack<string> sStack;
  52. iStack.push(7);
  53. cout << iStack.top() << endl; //7
  54. sStack.push("Hello World");
  55. cout << sStack.top() << endl; //Hello Worl
  56. cout << iStack.empty() << endl; //0
  57. iStack.pop();
  58. cout << iStack.empty() << endl; //1
  59. //空栈pop
  60. iStack.pop(); //Stack<> :: pop(): empty stack
  61. return 0;
  62. }
  63. catch(exception const& e){
  64. cerr << "Exception: " << e.what() << endl;
  65. return -1;
  66. }
  67. };

class 用于定义类,在模板引入 c++ 后,最初定义模板的方法 template……
class 关键字表明T是一个类型,后来为了避免 class 在这两个地方的使用可能给人带来混淆,所以引入了 typename 这个关键字,它的作用同 class 一样表明后面的符号为一个类型template……
在模板定义语法中关键字 class 与 typename 的作用完全一样。但typename 另外一个作用为:使用嵌套依赖类型(nested depended name),这个时候 typename 的作用就是告诉 c++ 编译器,typename 后面的字符串为一个类型名称,而不是成员函数或者成员变量,这个时候如果前面没有 typename,编译器没有任何办法知道 T::LengthType 是一个类型还是一个成员名称(静态数据成员或者静态函数),所以编译不能够通过。

  1. class MyArray
  2. {
  3. public
  4. typedef int LengthType;
  5. .....
  6. }
  7. template<class T>
  8. void MyMethod( T myarr )
  9. {
  10. typedef typename T::LengthType LengthType;
  11. LengthType length = myarr.GetLength;
  12. }

函数模板可以重载,只要它们的形参表不同即可。

  1. template<class T1, class T2>
  2. void print(T1 arg1, T2 arg2)
  3. {
  4. cout<<arg1<<" "<<arg2<<endl;
  5. }
  6. template<class T>
  7. void print(T arg1, T arg2)
  8. {
  9. cout<< arg1<< " "<< arg2<< endl;
  10. }

预处理器

define 预处理指令用于创建符号常量。该符号常量通常称为

  1. #define PI 3.14159 //替换PI为3.1459
  2. #define MIN(a,b) (a<b ? a : b) //参数宏。对ab进行三目运算
  3. //条件编译
  4. #ifdef NULL
  5. #define NULL 0
  6. #endif
  7. // #ifdef DEBUG 之前已经定义了符号常量 DEBUG,则会对程序中的 cerr 语句进行编译。
  8. #ifdef DEBUG
  9. cerr <<"Variable x = " << x << endl;
  10. #endif
  11. //使用 #if 0 语句注释掉程序的一部分
  12. #if 0
  13. //不进行编译的代码
  14. #endif
  15. #ifdef SOMETHING
  16. int func1(){/*...*/}
  17. #else
  18. int func1(){/*...*/}
  19. #endif
  1. //# 运算符会把 replacement-text 令牌转换为用引号引起来的字符串。
  2. #define MKSTR( x ) #x
  3. //## 运算符用于连接两个令牌,CONCAT(HELLO, C++) 会被替换为 "HELLO C++"
  4. #define CONCAT( x, y ) x ## y

# 字符串化的意思,出现在宏定义中的#是把跟在后面的参数转换成一个字符串。
当用作字符串化操作时,# 的主要作用是将宏参数不经扩展地转换成字符串常量。

  • 宏定义参数的左右两边的空格会被忽略,参数的各个 Token 之间的多个空格会被转换成一个空格。
  • 宏定义参数中含有需要特殊含义字符如”或\时,它们前面会自动被加上转义字符 \。

## 连接符号,把参数连在一起。
将多个 Token 连接成一个 Token。要点:

  • 它不能是宏定义中的第一个或最后一个 Token。
  • 前后的空格可有可无。

预定义宏:

描述
LINE 这会在程序编译时包含当前行号。
FILE 这会在程序编译时包含当前文件名。
DATE 这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。
TIME 这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。

信号处理

信号 描述
SIGABRT 程序的异常终止,如调用 abort
SIGFPE 错误的算术运算,比如除以零或导致溢出的操作。
SIGILL 检测非法指令。
SIGINT 程序终止(interrupt)信号。
SIGSEGV 非法访问内存。
SIGTERM 发送到程序的终止请求。

signal()函数,捕获突发事件

  1. void (*signal (int sig, void (*func)(int)))(int); //第一种
  2. signal(registered signal, signal handler) //第二种

函数接收两个参数:第一个参数是一个整数,代表了信号的编号;第二个参数是一个指向信号处理函数的指针。
捕捉信号需要提前注册信号。

  1. void signalHandler( int signum )
  2. {
  3. cout << "Interrupt signal (" << signum << ") received.\n";
  4. // 清理并关闭
  5. // 终止程序
  6. exit(signum);
  7. }
  8. int main ()
  9. {
  10. // 注册信号 SIGINT 和信号处理程序
  11. signal(SIGINT, signalHandler);
  12. while(1){
  13. cout << "Going to sleep...." << endl;
  14. sleep(1);
  15. }
  16. return 0;
  17. }

多线程

  • 基于进程的多任务处理是程序的并发执行。
  • 基于线程的多任务处理是同一程序的片段的并发执行。