920 likes | 1.15k Views
C++ 高级编程. Overview. Module 1 : 面向过程的 C++ Module 2 : 面向对象的 C++ Module 3 : C++ 设计模式. Module 1 : 面向过程的 C++. 从 C 到 C++ 内存管理 数组 字符集和字符串 指针和引用 函数 其它. 从 C 到 C++. 语法增强 新的运算符 变量声明更加灵活 函数重载 引用 类型安全性 面向对象 封装 继承 多态. 内存管理. 在 stack 上分配内存 简单数据类型 不使用 new 创建对象实例 函数调用完成自动销毁 线程堆栈的限制
E N D
Overview • Module 1 : 面向过程的C++ • Module 2 : 面向对象的C++ • Module 3 : C++设计模式
Module 1 : 面向过程的C++ • 从C到C++ • 内存管理 • 数组 • 字符集和字符串 • 指针和引用 • 函数 • 其它
从C到C++ • 语法增强 • 新的运算符 • 变量声明更加灵活 • 函数重载 • 引用 • 类型安全性 • 面向对象 • 封装 • 继承 • 多态
内存管理 • 在stack上分配内存 • 简单数据类型 • 不使用new创建对象实例 • 函数调用完成自动销毁 • 线程堆栈的限制 • 在Heap上分配内存 • malloc 还是 new ? • 检测内存泄漏 • 防止”野指针”:回收内存后指针会自动为NULL吗? • Demo: 动态扩充的字符串缓冲区
数组 (1) 声明及初始化 int a[]={1,2,3}; int b[3]={1,2,3}; int c[5]={1,2,3}; // c[3]和c[4]的值默认为0 int a1[2][3]={{1,2,3},{4,5,6}}; int a2[2][3]={{1},{}}; // 初始化,第一行为1,0,0;第二行为0,0,0 int a3[][3]={{},{1,2,3}}; int a4[2][]={{1,2,3},{4,5,6}}; // 错误。必须声明第二维的数目 int a5[][3]={1,2,3,4,5,6}; // 正确
数组 (2) 数组的内存形式 • 数组在内存中的存放 • 全局数组在静态存储区域中存放 • 局部数组,一般在堆栈上存放 • 在堆上动态分配内存 • 线程堆栈的限制:double arr[500][300] ? double *p = new double[10]; int n = 10; double *p = new double[n]; // n不必是常量 double *p = new double[400][300]; // 错误 double **p = new double[400][300]; // 错误
数组 (3) 指针表现形式 const int ARRAY_SIZE = 5; int a[ARRAY_SIZE] = {1,2,3,4,5}; int *p = a; 考察: (1) a+i, p+i, *(a+i), *a+i, p[i] 的含义 (2) p++偏移几个字节(4个)?a++呢?//a++是非法操作 • 一维数组指针表示 • 二维数组指针表示 • 行地址和列地址 int a[ ][3] = {{1,2,3},{4,5,6}}; 考察:a, a+i, *(a+i), *(a+i)+j, *(*(a+i)+j),&a[ i ][ j ]的含义//a+i和*(a+i)值相同,但是 含义不同,前者是第i行地址,后者是第i行第1个元素的地址. 因此a+i+j和*(a+i)+j也不同,前者是第i+j行地址,后者是第i行第j个元素的地址
数组 (4) 二维数组的指针表示 • 使用一级指针访问二维数组 • 使用指向一维数组的指针来访问二维数组 int a[ ][3] = {{1,2,3},{4,5,6}}; int *p=a[0]; // 将二维数组展开成一维数组的方式访问 int *p = a; // 错误! 考察: p+i, *(p+i), *p+i的含义 int (*p)[3]=a; // 也可写成:int (*p)[3](a); int *p[3]=a; // 错误! Int* p[3]; 考察:p, p+i, *(p+i), *p+i, *(p+i)+j, *(*(p+i)+j)的含义 //
数组 (5) 动态创建复杂数组 • 二维数组 • 交错数组 int m = 2;int n = 3; int (*p)[3] = new int[2][3]; delete[] p; int* p[m]; // p数组包含两个元素,每个元素都是int*类型 p[1] = new int[3]; // 通过p[ i ]来操作每个数组 delete p[1]; // 不能使用 delete p或者delete[] p
字符集和字符串 (1) 字符编码 • ANSI、UNICODE和UTF-8 • MultiByte和WideChar • wchar_t, “L”前缀, wcslen, wcscpy, wcslen • 字符集的转换 • mbstowcs_s和wcstombs_s • MultiByteToWideChar, WideCharToMultiByte wchar_t p1[] = L“abcd”;考察:wcslen(p1)和sizeof(p1)的结果 wchar_t p1[] = L"abcd"; char p2[10]; int len = wcstombs(p2, p1, sizeof(p1)); // len = 4 ’\0’字符不计入拷贝字符数目 wchar_t *p3 = (wchar_t*)malloc( 100); mbstowcs(p3, p2, 100);
字符集和字符串 (2) 兼容字符集 • Windows平台与字符集 • Visual C++编译器字符集设置选项 • _UNICODE • _MBCS • 兼容字符集 • TCHAR, _tcslen, “_T” TCHAR t[] = _T("aaa"); // 如果编译器定义了_UNICODE,sizeof(t)=8, // 如果定义了_MBCS,sizeof(t)=4
字符集和字符串 (3) 声明字符串 char *p1 = "abcd"; char *p2 = "abcd"; 考察:内存中有几个”abcd”存在, p1 == p2是否成立?//相等 • 使用指针 • 使用数组 char p1[10] = {‘a’,‘b’,‘c’,‘d’,‘\0’}; // 注意结尾的’\0’ char p2[] = {“abcd”}; // 自动添加’\0’结尾 char p3[] = {“abcd”}; char p4[] = {'a','b','c','d'}; // 仅仅声明一个字符数组而非字符串 考察(1) sizeof(p), strlen(p)的值分别是多少?//sizeof(p1)=10,strlen(p1)=4 (2) p2 == p3 ? 不等
字符集和字符串 (4) 字符串操作函数 char p1[10] = “abcd”; char* p2 = “abcd”; // p2指向常量字符串 p1[0] = ‘x’ // OK p2[0] = ‘x’ // 错误,常量字符串不可修改//捕获异常?? • 修改单个字符 • 字符串长度 • 模拟编写strcpy方法 • 为何采用const作为参数 • 为何返回char* • ‘\0’会被拷贝吗?//自己控制呗 • Demo char p1[] = "a\t\nc";考察: strlen(p1) 和sizeof(p1) 4,5
const • Const常量有数据类型,编译器会进行类型安全检查. • 需要公开的常量放在头文件中,否则放在自定义的文件中 • C++严格区分定义和声明,所以类中的常量赋初值在构造函数的初始化列表而不是在函数体中.只有静态常量例外static const ,它可以赋初值 Class A { Const Int SIZE; A(int size): }; A::A(int size):SIZE(size) { }
const Class A { Const Int SIZE; A(int size):}; • 建立在整个类中都恒定的常量,用枚举 枚举常量不会占用对象的存储空间,它们在编译时被全部求值。 枚举常量的 缺点是:它的隐含数据类型是整数
指针与引用 (1) 概念 double a = 10; double* p = &a; 考察: (1)p是指针变量,则sizeof(p)=?//指针都占4个字节 (2)&p含义是什么? • 指针与指针变量 • 引用的定义 • 引用必须在声明时立即初始化,不允许空引用 • 引用一旦初始化,就不能再引用其它数据 • 引用和被引用的变量实际上代表同一个内存的数据 int a = 100; int& b = a; b = 1000; 考察: (1)&a与&b的关系?//相等对应于同一个地址 (2) a=?
指针与引用 (1) 概念 • 引用的主要功能 传递函数的参数和返回值.C++中常用的方式有三种 • 值传递,指针传递和引用传递 • 引用传递的性质象指针传递,书写形式象值传递, • 理由:如果只需要借用一下别名,就没必要用指针,. • 用适当的工具做恰如其份的事情 Void f(int &x) {X++;} Void main() { int n=10; f(n);}
指针与引用 (2) 混合使用 double a = 100; double& b = a; double *p = &b; 考察:(1)&a, p和&b的关系 //相等(2)修改a, b或*p的值,另两个是否联动 //联动 • 指针指向某个引用 • 引用一个地址 double a = 100; double& b = a; double *p = &b; double* &rp = p; // p和rp是完全相同的,且都是变量a的地址 考察:sizeof(b)=? 8 sizeof(rp) = ? 4
指针与引用 (3) 作为参数传递 • 按值传递 • 按地址传递 • 按引用传递 void Increase(int& data) {data++; } void main() { int mydata = 10; Increase(mydata); }
指针与引用 (3) 作为参数传递 • 如果参数是指针,且只作输入用,则应在类型前加const.以防止该指针在函数体内被意外修改. • 如果参数是以值传递,改成用const &,可以省去临时对象的构造和析构 • 有时函数返回是为了增加灵活性,如支持链式表达,可以附加返回值. • Return语句不可返回指向”栈内存”的指针或引用,因为该内存在函数结束时被自动销毁. • Assert是仅在debug版本起作用的宏,当参数为 假的时候,终止.需加头文件assert.h
指针与引用 (4) 指针与常量 int a =100; int b = 200; const int* pci = &a; // pci不能直接修改a地址中的数据 a = 1000; // 可以通过修改a的数据从而间接修改*pci *pci = 1000; // 错误 pci = &b; // OK. Pci可以指向另一个地址,不再与a相关 考察: 如果a被定义为:const int a = 100,那么: int* pci = &a; 是否成立?//不 成立 • 指向常量的指针 • 指针常量 inta =100; int b = 200; int* const pci = &a; // pci不能再指向别的地址 *pci = 1000; // 可以通过修改*pci来修改a的数据 pci = &b; // 错误 考察:如果有 const int* const pci = &a, 则情况如何?
指针与引用 (4) 引用与常量 • 将一个引用设置为常量后,不能通过该引用修改数据;但仍可通过被引用的变量来改变: • 被引用的数据是常量,引用本身也必须是常量: int a = 100; const int& b = a; // b与a是同一内存,但不能直接修改b b = 1000; // 错误 a = 1000; // OK const int a = 100; const int& b = a; // OK int& b = a; // 错误
指针与引用 (5) Practice char s1[] = "abcdefg"; char s2[] = "1234567"; char* p1 = s1; const char* p2 = s1; char* const p3 = s1; const char* const p4 = s1; 考察:下列哪些语句有效: p1[0]=’k’; p2[0]=’k’; p3[0]=’k’; p1=s2; p2=s2; p3=s2; p4=s2;
函数 (1) Extern • C语言中的Extern和Static • C++中的extern “C” • 允许在C++中调用C编写的函数 • 允许在C中调用C++编写的函数 • Demo: 如何在C++和C之间实现互操作? cfile.h文件: extern int add(int x, int y) cfile.c文件: int add(int x, int y) {return x + y;} cpp.cpp: extern “C” { #include “cfile.h”} …… 其它格式: extern “C” int add(int x, int y); extern “C” { int add(int x, int y); int sub(int a); }
函数 (2) 函数指针 int (*fun)(int x,int y); 或者 int (*fun)(int,int); • 声明原型 • 指针赋值 • 函数调用 • 函数指针作为参数传递 设存在函数 int max(int i, int j) {… …} int (*fun)(int,int)(max); 或者fun=max; (*fun)(10,20); void process(int x, int y, int (*f)(int,int)) { … …} process(10,20,max);
函数 (3) 函数指针 设有函数定义:intmyequal1(char* s1,char* s2) {...} 类型定义:typedef int (*equal)(char*,char*); 则可以声明: equal eq=myequal1; 并且调用: (*eq)(s1,s2);
函数 (4) 缺省参数 • 注意 • 缺省参数必须出现在所有非缺省参数的后面 • 防止函数重载时的二义性 void f(int i, int j=10, int k=20) { … … } 调用: f(1,20),相当于:f (1,20,20) 而: f(1), 相当于:f(1,10,20)
函数 (5) 参数个数不定 • 回顾 printf(“%s”, ……) • 编写自定义得可变参数个数函数 (stdarg.h) int average( int first, ... ) { int count = 0, sum = 0, i = first; va_list marker; va_start( marker, first ); while( i != -1 ) // 为方便起见,假定参数以-1结尾 { // 实际工作中应通过某种方式确定参数的个数 sum += i; // 例如printf就是通过格式化串中的%确定的 count++; i = va_arg( marker, int); } va_end( marker ); return( sum ? (sum / count) : 0 ); }
其它 • sizeof • 针对字符串的sizeof • 针对数组和指针的sizeof • 针对结构体的sizeof • 变量赋值 • a=b=c=3 ? • int *x, y ? int* x, y • const 和 define • 作用域限定运算符 • 无名共用体 • 枚举
Module 2 : 面向对象的C++ • 构造和销毁 • 参数和返回值 • const修饰符 • 运算符重载 • 虚函数 • 友元 • 模板 • 其它
构造和销毁 (1) 数据声明和初始化 class A {public: A():d2(10) { /* d2 = 10 error! */ } static int d1; // static int d1 = 100; error! const int d2; // const int d2 = 10; error! static const int d3 = 200; }; int A::d1 = 100; // const int d3 = 200;
构造和销毁 (2) 初始化列表 class B class C : public B {public: {public: B(){ i = 0;} C() { j = 0;} B(int i):i(i){} C(int i, int j) : B(i), j(j){} int i; }; int j; }; • 成员变量较少的情况下尽量使用初始化列表 • 各变量在初始化列表中出现的顺序最好与变量声明的顺序一致
构造与析构 (3) 拷贝构造函数 • 函数原型 • 调用场合 • 默认拷贝构造函数 • 成员变量之间的“值”拷贝 A ( const A& other) { … … } A a1(10); // 构造函数 A a2 = a1; // 拷贝构造函数 A a3(a1); // 拷贝构造函数
构造与析构 (4) 拷贝构造函数 Class A { private: char* name; public: A(const char* data) { name = new char[strlen(data) + 1]; strcpy (name, data);} A(const A& other) { name = new char[strlen(other.name) + 1]; strcpy(name, other.name);} }; 考察:char* data = “abcd”; A a1(data); A a2 = a1; 如果未定义拷贝构造函数,会有何种后果? • 编写拷贝构造函数的必要性
构造与析构 (5) 赋值函数 A& operator =( const A& other) { … … } • 赋值函数原型 • 调用场合 • 编写赋值函数的必要性 A a1(10); // 为a1调用构造函数 A a2; // 为a2调用默认构造函数 a2 = a1; // 为a2调用赋值函数。 区别 A a2=a1; A& operator =(const A &a){ if (&a == this) return *this; //... 具体赋值操作 return *this;} 考察: (1)为何首先检查同一性? (2) (a=b)=c或者a=(b=c)是否合法 (3)若定义为void operator =(const A &a)有何局限? (4)若定义为A operator =(const A &a){...return *this;}有何局限?
构造与销毁 (5) 初始化列表与拷贝构造函数 • 子对象在构造函数列表中初始化 • 直接调用拷贝构造函数 • 子对象在构造函数中赋值 • 先调用子对象的默认构造函数 • 调用赋值函数为子对象重新赋值 class A{ B b; public: A(B b1){ b = b1; /*先调用默认构造函数,再调用赋值函数 */ } A(B b1) : b(b1) /*调用拷贝构造函数*/} }
构造和销毁 (6) 构造和析构顺序 • 调用基类构造函数 • 为各子对象调用构造函数 • 如果有多个子对象,则各个子对象构造顺序与声明顺序一致,而不依赖初始化列表中出现的顺序 • 本类构造函数 • 析构函数的顺序与构造函数相反 • 如果没有显式调用基类或子对象构造函数,那么将调用其默认构造函数;如果默认构造函数不可访问,则编译错误
构造与销毁 (7) 内存回收 • 析构函数 • 作用 • 何时调用 • 通过指针使用对象 • new-delete • NULL指针与野指针 • Demo • 构造函数、拷贝构造函数、赋值函数、析构函数综合使用实例分析
参数和返回值 (1) 传递数组 void f(char* arr) { arr[0] = ‘A’; }// 修改可以成功 void f(char* arr){ arr = new char[10]; // 修改无效 strcpy(arr, "12345"); } void f(char** arr){ *arr = new char[10]; // 修改可以成功 strcpy(*arr, "12345"); }
参数和返回值 (2) 传递对象实例 • 实参对象拷贝给形参对象 • 实参与形参不是同一个对象 class A{ public: int age; }; void f(A obj) // obj调用拷贝构造函数将实参a拷贝给自己 { obj.age = 10; // 实际修改的是obj对象的值,而非实参a对象 } void main() { A a; a.age = 20; f(obj); // a.age仍然是20 }
参数和返回值 (3) 传递对象引用或指针 void f(A& obj) // obj是对实参a的引用,二者同一个地址 { obj.age = 10; // 修改obj就是修改a } • 省去了参数对象之间的拷贝,提高效率 void f(A* pobj) // pobj指向a所在的地址 { pobj->age = 10; // 通过指针修改a }
参数和返回值 (4) 返回对象实例 • 理解隐含的构造函数、拷贝构造函数、赋值函数调用 A CreateA() { A obj; // 在堆栈创建局部对象 obj.age = 10; return obj; // obj被拷贝给一个临时对象tmp } // 函数结束,堆栈弹出,局部对象obj被销毁 调用: A a; // 调用A的默认构造函数 a = CreateA(); // 调用a的赋值函数将tmp对象赋给a A b = CreateA(); // 情况会有所不同,obj直接通过拷贝构造函数拷贝给b
参数和返回值 (5) 返回对象指针 A* CreateA() { A obj(); obj.age = 10; return &obj; } // obj将被销毁,因此返回的地址实际上没有意义 调用: A *p = NULL; // 这不会引起调用A的默认构造函数 p = CreateA(); // 没有临时对象产生,没有拷贝构造函数和赋值函数调用 考察:既然p得到的地址没有意义,为什么有些情况下通过p来 访问对象的数据,仍然可以成功呢? • 返回无效的对象指针
参数和返回值 (6) 返回对象指针 A* CreateA() { A *pObj = new A(); pObj->age = 10; return pObj; } // 由于pObj分配再堆而不是堆栈上,因此不会被自动销毁 调用: A *p = NULL; // 这不会引起调用A的默认构造函数 p = CreateA(); delete p; // 释放内存 • 返回有效的对象指针
参数和返回值 (7) 返回对象引用 • 返回对象引用可以避免临时对象的产生 • 但是要特别注意,不要返回无效的引用 A& CreateA() { A obj; obj.age = 10; return obj; // obj将被销毁 } A& b = CreateA(); // b引用obj,但obj已经销毁,因此b无效 考察:为什么赋值函数中可以并且应该返回引用?
参数和返回值 (8) 编写高效的return • 直接在return语句中构造对象将有助于减少临时对象的创建和销毁 A GetA() { //A a(10); // 创建临时对象 //return a; // 调用拷贝构造函数将临时对象赋给obj,然后销毁临时对象 return A(10); // 直接把临时对象创建在obj的内存单元中z } A obj = GetA();
const修饰符 (1) 修饰参数和返回值 • 修饰参数 • 编译器确保该参数在函数内不会被修改 • 修饰返回值 • 返回结果是常量,不能修改 考察: f(int i) 与 f(const int i)有何区别? const complex operator+(const complex& c1, const complex& c2) { return complex(c1.real+c2.real,c1.image+c2.image); } 考察: (1)c1和c2声明为常量引用有什么好处? (2)设obj1,obj2,obj3都是complex,则(obj3+obj2) = obj1是否合法? (3) 函数为什么不返回一个引用?
const修饰符 (2) 常函数 • 对于一个不会修改数据成员的类成员函数,建议声明为常函数 • 常量实例只能调用函数的const版本 • 非常量实例一般调用函数的非const版本 • 如果一个函数只有const版本而没有非const版本,那么非常量实例可以调用const版本 • Const函数内部不能再调用非const函数 int GetModule() {return real*real + image*image;} int GetModule() const {return real*real + image*image;} 如果:complex c1(1,2); c1.GetModule(),则调用非const const complex c2(1,2); c2.GetModule(),则调用const
运算符重载 (1) 实现方式 Complex& operator +=(Complex c){ this.real += c.real; this.image += c.image; return *this; } • 重载为类实例成员函数 • 重载为友元函数 • 考察:重载了”+”后,还需要重载”+=“吗? friend Complex operator +(Complex& c1,Complex& c2){ // 友元是全局函数,应该生成新对象返回,而不能返回*this return Complex(c1.real+c2.real,c1.image+c2.image); }