C++基础知识点
1 "天命不足畏,祖宗不足法,人言不足恤"
2 3 第一天 C++基础班 ************************************============================================= 4 1.封装,继承,多态。 5 2.作用域运算符(::) 6 :: 前面不写东西,表示调用全局变量 7 3.名字控制(命名空间(namespace)): 8 1)命名空间只能在全局范围内定义 9 2)命名空间可以嵌套命名空间 10 3)命名空间是开放的,可以随时向命名空间添加东西(变量,函数,类等)。 11 4) 声明和实现可分离 12 5) 匿名命名空间,默认此空间中的函数,变量等只在当前文件中有效,相当于给每个变量、函数前加入static. 13 namespace TestA 14 { 15 int a; //这是在命名空间中未初始化的定义。 当在命名空间进行定义时,而且此命名空间放在头文件,当在其他文件进行 16 //调用时,会产生重复定义的问题,编译不过。 17 int b = 9; //这是在命名空间中初始化的定义 18 extern int c; //这是命名空间中的声明。 19 } 20 21 4.条件编译 22 #pragma once (第一种) 23 #ifndef ___ ; #define _____; #endif; ____ 代表一个定义的名字。(第二种) 24 25 5.bool 类型: bool 类型的变量可以把"任何非0值"转换为1. 26 27 6.三目运算符:C和C++的区别: 28 C语言中,三目运算符返回的是一个变量的值,没有内存空间,不可以修改。 29 int a = 10; int b = 20; a > b ? a : b; (a > b ? a : b) = 30(不可以这样操作,原因如上); 30 C++语言中,三目运算符返回的是一个变量本身,具有内存空间,可以修改。 31 int a = 10; int b = 20; a > b ? a : b; (a > b ? a : b) = 30(可以这样操作,原因如上) 32 33 7.当一个变量有内存空间时(只要在堆栈区),就可以通过它的地址修改内存里的值 34 35 8.const 的使用 36 如:C语言中,const 修饰的变量是一个只读变量,具有内存。 37 C++语言中,const 修饰的为常量不具有内存(有额外情况,下面有),它放在符号表中。 38 39 1)C++ 中两种情况不一样 40 const int a = 10; 此时 a 不具有内存,(用一个常量初始化)它放在符号表中。所以不可以对它进行修改。 41 int b = 9; const int a = b; 此时a 具有内存(在栈区),因为对它进行赋值的是一个变量b(栈区),可以通过指针间接赋值改变 a 的值。 42 (上面这种情况为用一个变量初始化 const 变量,所以它可以修改)。 43 44 2)在局部变量中。( C语言 和 C++ 语言这种情况不一样) 45 C++语言中: const int a = 9; int *p = (int *)&a; *p = 99; 46 此时当对 const 常量 a 取地址,编译器会为它分配地址(在栈区);但是 a 常量从符号表中取值,不可被修改。 47 *p 修改的只是分配的内存空间的值。打印结果: a = 9; *p = 99; 48 C语言中,变量 a 可以被修改。 49 50 3)在全局变量( C语言 和 C++ 语言这两种情况一样) 51 extern const int a = 9; int *p = (int *)&a; *p = 99;(第一种) 52 const int a = 9(它放在全局变量); int *p = (int *)&a; *p = 99(第二种); 53 此时对 const 变量 a 取地址或声明 extern 时,编译器会为它分配地址(在只读变量区);这段代码可以编译通过,但是运行阶段会报错,不 54 以被运行。 55 56 4)C语言中 const 修饰的变量为外部链接(和普通变量一样)。 57 C++语言中,const 修饰的变量为内部链接(和普通变量不一样)。 58 不管是C语言还是C++ 语言,普通变量都是外部链接。 59 60 5)const 与 #define 的不同, const 有类型,有作用域,可进行编译器类型安全检查。 61 #define 无类型,不重视作用域(从定义开始到文件结尾),不可以进行安全检查。 62 63 9.extern 的使用 64 适用范围是不同文件之间变量的调用。(不可以再同一文件不同函数之间使用) 65 在不同文件之间调用一个外部链接的变量时,只用在调用的文件中写一个 extern 关键字就可以了。 66 当调用一个内部链接时,需要在被调文件和调用文件中都写 extern 关键字。 67 68 10.变量可以定义数组 int n = 9; int arr[n]; 69 :不同编译器编译环境效果不同。在vs2013中不可以通过;在QT,linux中可以通过。支持C99的编译器可以通过。 70 71 11.using 编译指令 与 using 声明的区别 72 namespace A{ int a = 9; func(){} } 73 namespace B{int a = 9; func(){} } 74 using 声明: int main(){using A::a; cout << a <<endl; int a = 6;}当第二个 a 被定义时,函数编译通不过,命名冲突 75 using 编译指令:int main(){using namespace A; cout <<a<<endl; int a = 6; cout << a << endl;} 打印结果为:9, 6; 76 此时打印最近定义的那个。 77 int main(){ using namespace A; using namespace B; cout << a << endl; } 此时就会报错。编译器不知道选择哪个a 78 注意:使用using声明或using编译指令会增加命名冲突的可能性。也就是说,如果有名称空间,在代码中 79 使用作用域解析运算符,则不会出现二义性。 80 81 12.在 C 语言中,重复定义多个同名的全局变量是合法的(不正规),但在C++ 中是不合法的。 82 83 84 "天命不足畏,祖宗不足法,人言不足恤" 85 第二天 C++基础班 ************************************============================================= 86 87 1.变量都是有内存空间的,当它在堆栈区时可以通过指针修改它的值。 88 89 2.引用:本质地址传递;编译器帮忙做了基本地址传递的部分。 90 int &b = a;=====> int * const b = &a;(实质) b常指针,指向不可以改变。 91 (本质上:引用是一个常指针) 92 1)同一个内存块可以取多个别名。int& b = a; int& c = a; 93 2)引用的基本语法:int a = 10; int &b = a;(引用) b = 100; cout << a << endl; cout << b << endl; 94 输出的结果都为 100. 95 3)指针的引用; int *p = NULL; int *&p1 = p; p1 = (int *)malloc(sizeof(int)); 96 4)数组的引用:(两种方式) 97 1. typedef int ARR[10];/*建立数组类型*/ int a[10];/*创建一个数组*/; ARR &p = a;/*对数组的引用*/ p[i] = i;/*对数组的某个元素赋值*/ 98 2.int (&p1)[10] = a;/*直接建立数组类型,进行引用,并初始化。*/ p1[i] = i; /*对数组的某个元素赋值*/ 99 "注意内容:" 100 5) 引用没有定义,是一种关系型声明。声明它和原有某一变量(实体)的关系。故 而类型与原类型保持一致,且不 101 分配内存。与被引用的变量有相同的地址 102 声明引用变量时必须初始化, int& b; //错误 103 必须确保引用是和一块合法的内存块关联( NULL 不可内存不可引用)。 104 可以建立数组引用。 105 6)引用一旦初始化,不能改变。(原因如上:本质) 106 int &b = a; b = c (这种写法并不是改变b的指向,是将c的值赋给 b; b并没有指向c); 107 7)(常量引用)const int &b = a; b的值、指向都不能改变。 108 因为它等同于: const int *const b = &a;(这种写法常用来保护"实参"不受"形参"的改变). 109 8)C++编译器在编译过程中使用"常指针"作为引用的内部实现,因此引用所占用的空间大小与指针相同。(非官方的说法,但是大家都这么说) 110 9) 111 //建立普通变量的引用 112 int ma = 9; 113 int &ra = ma; //建立引用。 ===》 int *const ra = &ma; 即 ra = &ma; 114 ra = 88; //通过引用修改变量的值。这步是编译器帮忙进行解引用,然后赋值的。 115 116 //建立对指针的引用 117 int *p2 = NULL; 118 int *&mp2 = p2; //建立引用。===》 int ** const mp2 = &p2; 119 mp2 = (int *)malloc(sizeof(int)); //通过引用给指针p2分配空间。也就是给p2重新赋值,让他重新指向。 120 121 //建立对数组的引用 122 typedef int Arr[10]; /*建立数组类型 */ Arr a; //建立一个普通数组 123 Arr &p3 = a; //建立引用 ===》 Arr * const p3 = &a; 124 p3[3] = 10; //对数组的第四个元素赋值。 125 126 3.引用的几点基本知识: 127 1)单独定义引用时,必须初始化;说明它很像一个常量,因为常亮在定义时也必须初始化(const int a = 5)。 128 2) 普通引用有自己的空间。(在32位平台下占4个字节。)但是引用变量的地址和初始化它的变量是同一块地址。 129 int &a = b; a 和 b 的地址相同。() 130 struct teacher {int a; char b; int &d; double &c; }; 这个结构体所占内存为16; 131 struct teacher {int a; char b; }; 这个结构体所占内存为8; 132 3)引用的本质是一个常量指针。 133 4) 134 135 3.函数中的引用:引用做函数的参数,引用做函数的返回值 136 1)引用做参数不需要初始化 137 2)不能返回局部变量引用;(和返回局部指针变量原因一样)。 138 3)引用做返回值。(可以做左值和右值) 139 4)指针的引用。 140 5)(常量引用):const 对引用的使用(如上:)。const 引用的值不能修改(主要用在函数的形参:不想用形参改变实参的值) 141 6)"函数的返回值当左值需要返回引用。" 142 143 4.类: 144 1)使用class关键字 145 2)类里面可以放变量、函数。 146 3)public: 访问权限 147 4) 148 149 5.内联函数:C++中使用(既有宏函数的效率,又没有普通函数的开销;可以像普通函数那样,进行参数,返回值类型的安全检查,又可以 150 作为成员函数。 在C++中,定义内联函数,只是对编译器的一个建议,并不一定会成为内联函数。 151 (内联函数的语法) 152 1)普通函数; inline void func(int x){ return; }但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。 153 2)要求:不能存在任何形式的循环语句;不能存在过多的条件判断语句; 函数体不能过于庞大; 不能对函数进行取址操作 154 155 6.宏函数:C语言中使用 156 1) #define ADD(X, Y) X+Y 157 int main() {int ret = ADD(10,20); return 0;} 158 2)副作用很多:无脑替换,不检查语法;没有作用域,从定义开始到文件结束 159 160 161 7.函数的默认参数和占位参数: 162 1)默认参数:int func(int x = 10,int y = 20);此时形参的赋值就是默认参数,当函数调用不传参数时,就将使用默认参数。。 163 2)注意: 164 int func051(int x, int y = 0, int z = 0);//函数的默认参数从左向右,如果一个参数设置了默认参数,那么这个参数之后的参数都必须设置默认参数。 165 函数的声明和函数的定义不能同时写默认参数,("即使默认参数相同也不行")编译器不知道该选择哪套 166 3)占位参数: 167 "函数的占位参数也是参数,必须要给个值,只是函数内部用不了而已. " int func(int , int y);或 int func(int ,int y = 3); 168 当占位参数与默认参数结合时:int func(int =5, int y);"erro" ;这种写法,int (int y, int =30);占位参数只能写在最后一个形参的位置。 169 而且可以不给它传参。因为它有了默认参数。 170 171 8.函数重载: 172 1)函数重载的条件: 173 "可以作为条件的:" 174 同一个作用域:函数名相同,形参的个数、形参的类型、形参的类型顺序不同; 175 用 const 进行修饰的函数也可以进行重载。 非 const 对象优先调用非 const 函数。 176 const 对象只能调用 const 函数,const 函数只能调用 const 函数,可以被 const 函数和非 const 函数调用。 177 注意:不可以作为条件的 178 "函数的返回值不能作为函数重载的条件" 179 "函数重载和默认参数不能同时出现";函数重载碰到默认参数,那么要考虑是否会出现函数调用二义性(会报错,编译通不过)。 180 2)重载函数的调用: 181 正常调用:"函数调用正常匹配函数形参(可以找到)。" 182 隐式类型转换后调用:"当找不到匹配的形参时,编译器会进行隐式转换,仍然找不到后会进行报错。(如下)" 183 void func(char b); int main(){ int a = 3; func(a);} 此时就会进行隐式转换,因为与ASCII码匹配。 184 185 3)函数重载的原理: 186 编译器为了实现函数重载,在编译的时候做了一些优化,用不同的类型来修饰不同的函数名。 187 如:void func(){} 188 void func(int a){} 189 void func(int a,char b){} 190 上述三个函数编译完后:生成的函数名为:_z4funcv "v代表void,无参数" 191 _z4funci "i代表参数为int类型" 192 _z4funcic "i代表第一个参数为int类型,第二个参数为char 类型" 193 194 195 "天命不足畏,祖宗不足法,人言不足恤" 196 第三天 C++基础班 ************************************============================================= 197 198 1.struct 的区别(C 和 C++): 199 C语言中只能定义变量。 200 C++语言中可以定义变量和函数。同时C++语言中,struct 中所有变量和函数都是 "public" 权限 201 2.类的封装: 202 3.类内部的三种权限 203 public:共有属性(修饰的成员变量和方法; 可以在类的内部和外部使用。) 204 private:私有属性(修饰的成员变量和方法,只能在类的内部使用,不能在类的外部使用) 205 protected:主要用于继承,保护属性(修饰的成员变量和方法; 可以在类的内部和继承的子类使用,不能在类的外部使用) 206 4.struct 和 class 的区别: 207 struct 中成员的默认权限为 public; 208 class 中成员的默认权限为 private; 209 5.类的调用(一个类调另一个类) 210 6.对象的构造和析构; 211 1)构造函数:"名称和类名相同";"没有返回值 "。"可以有多个,进行函数重载" 212 在内存开辟之后调用构造函数。 213 214 2)无参构造函数和有参构造函数(又分为两种) 215 1.无参构造函数:定义对象的时候,对象后不能加括号。如:class stu{ stu(){"无参构造函数"}}; stu st;(正确定义对象)。 216 如果加上括号: stu st(); 编译的时候不会报错,(因为编译器把它当作函数声明),运行的时候会报错。 217 2.有参构造函数: 218 "普通参数": 根据形参的类型和个数,也可以进行函数重载。 219 class Animal{ 220 public: 221 Animal(int age) 222 { 223 cout << "一个参数构造函数数字!" << endl; 224 mName = "undefined"; 225 mAge = age; 226 } 227 Animal(string name) 228 { 229 cout << "一个参数构造函数字母!" << endl; 230 mName = name; 231 mAge = 0; 232 } 233 Animal(string name,int age) 234 { 235 cout << "两个参数构造函数!" << endl; 236 mName = name; 237 mAge = age; 238 } 239 //拷贝(复制)构造函数 用一个对象来初始化另一个对象 240 Animal(const Animal& animal) 241 { //拷贝构造函数,形参必须是引用。否则会造成死循环,一直调用它本身。本质上成了递归调用本身。 242 cout << "拷贝构造函数!" << endl; 243 mName = animal.mName; 244 mAge = animal.mAge; 245 } 246 "拷贝参数":用一个对象来初始化另一个对象 247 它的形参必须是引用。如果是普通变量,就会造成本质上的递归调用本身;无意义。 248 249 3)析构函数:"名称=类名+"~""; "没有返回值";"没有参数"。"一个类只可以有一个析构函数" 250 在内存释放之前调用析构函数。 251 7.构造函数调用规则: (参考上面的代码) 252 1) 括号法; 253 Animal animal1("smith"); //一个参数构造函数字母! 254 Animal animal2(20); //一个参数构造函数数字! 255 Animal animal3("John",30); //两个参数构造函数! 256 Animal animal4(animal3); //拷贝构造 257 2)显示调用构造函数(匿名对象调用拷贝构造的问题) 258 Animal("Smith",30); //匿名函数生命周期仅限于当前行;此行运行完后立即析构。 259 Animal animal5 = Animal(30); // 一个参数构造函数数字!;形参为普通变量 260 Animal animal5 = Animal("smith",90); 注意:/"两个参数构造函数!;拷贝构造。这行比上一行多了一个拷贝构造,是因为string容器的缘故。只有这时才会有拷贝构造" 261 Animal animal5(animal(30)); //一个参数构造函数数字!。 262 Animal animal5(animal(30,"smith")); "两个参数构造函数!;拷贝构造。" 原因同上,当不调用 string 参数时,它不会调用拷贝构造。"是由编译器处理的,具体处理方式不清楚" 263 "上面那两种情况,调不调用拷贝构造函数,主要看参数类型。===有string:就调拷贝构造函数;==== 没有string: 就不调拷贝构造函数====" 264 匿名对象如果有变量来接,它就是一个对象;"此时这个变量就相当于匿名对象的名称。" 265 匿名对象如果没有变量来接,他就是一个实例化对象。 266 Animal4("john",92); Animal(animal4); 此时当这两句在一起,编译器会报错,重定义。 267 3)等号法 268 Animal animal5 = 10; //不常用。调用一个函数构造函数 269 Animal animal5 = animal4l; “拷贝构造,常用 270 8.拷贝构造常用的两种调用方式: 271 Animal animal5 = animal4l; 272 Animal animal5(animal4l); 273 274 9.拷贝构造的调用时机: 275 1)对象以值传递的方式传递给函数参数。 void func(Animal animal2){ }; 用实参初始化形参,调用了拷贝构造函数 276 2)用一个对象初始化另一个对象。Animal animal5 = animal4l; 或者:Animal animal5(animal4l); 277 3)函数返回局部对象。(注意这里:debug模式 和release模式不一样)。 278 debug模式下:会调用拷贝构造,打印两个地址也不相同。 279 release模式下,不会调用拷贝构造,打印的地址相同,编译器这里做了优化,直接把原来的对象返回回去了。 280 281 Animal MyBussiness() 282 { 283 //局部对象 284 Animal animal; //无参构造 285 cout << &animal << endl; 286 return animal; //编译器先拷贝一份,然后返回。相当于返回一个匿名对象 287 } 288 void test03() 289 { 290 Animal animal = MyBussiness(); 291 cout << &animal << endl; 292 } 293 294 10.构造函数调用规则: 295 默认情况下,编译器至少为我们写的类增加三个函数 296 1)默认构造函数(无参,函数体为空) 297 默认析构函数(无参,函数体为空) 298 默认拷贝构造函数,对类中非静态成员属性简单 "值"拷贝。 299 2) 如果用户定义了"拷贝构造函数",C++不会再提供"任何默认构造函数"。 300 3)如果用户定义了"普通构造,"C++"不会"再提供"默认无参构造",但是"会提供默认拷贝构造"。 301 4)在构造函数中调用构造函数是一个危险的行为。本质上:里面的构造函数相当于一个"匿名对象"。生命周期只有本行(没有变量接的情况下)。 302 303 11.初始化列表:初始化成员列表只能在构造函数使用。 304 "构造函数的初始化列表顺序:" 305 1)当一个类中组合了其他类的对象,先执行被组合的构造函数。 306 2)如果组合对象有多个,来自不同的类,根据定义的顺序进行构造函数的调用,而不是初始化列表的顺序。 307 3)“被组合对象的构造顺序:与定义顺序有关系,与初始化列表的顺序,没有关系” 308 4)若类成员中有 const 修饰,必须在对象初始化的时候,给 const 元素初始化。此时就用到了"初始化列表"。 309 310 "析构函数:与构造函数的调用顺序相反。" 311 312 12.深拷贝和浅拷贝 313 class Person 314 { 315 public: 316 Person(int a, int b) :_b(b),_a(b){} "初始化列表" 317 "深拷贝" 318 Person(const Person &p1) 319 { 320 mstr = (char *)malloc(strlen(p1.mstr) + 1); "核心步骤" “会进行内存拷贝” 321 strcpy(mstr, p1.mstr); 322 _a = p1._a; 323 _b = p1._b; 324 } 325 326 "浅拷贝" 327 Person(const Person &p1) 328 { 329 mstr = p1.mstr; "这里只会把一个对象的指针地址,赋给另一个对象的指针。";“不会进行内存拷贝” 330 _a = p1._a; 331 _b = p1._b; 332 } 333 void print_func() 334 { 335 cout << _a <<" " << _b << endl; 336 } 337 private: 338 int _a; 339 int _b; 340 char *mstr; 341 }; 342 343 13.C++中 string 是一个类。 344 345 14.已知一个Print类,在该类的main()函数里执行语句 "Print a, b[2], *c;" 后;程序会自动调用该类的构造函数 4 次。 346 a会调用一次;b会调用两次;*c是空指针,没有开辟空间不会调用。 347 348 15. 349 350 "天命不足畏,祖宗不足法,人言不足恤" 351 第四天C++ ++++++======================================================================================= 352 353 1.explicit:修饰构造函数; 禁止编译器隐式转换 354 1)针对单参数的构造函数 2)或者除第一个参数外其他参数都有默认值的多参构造函数。 355 356 2. C++ 中动态分配内存: 357 new :在堆上分配内存。分配对象的堆内存时会调用构造函数 358 申请string类型的空间只能string类型数据,可改为string *ps = new string(“10”); 359 delete:释放堆内存,在释放堆内存前会调用析构函数。 360 delete void*指针,不会调用析构函数。 361 362 3.创建动态数组: 363 int * arr = new int[10]; 创建一个 int 类型数组,元素10个。 364 delete [] arr; 释放内存。 365 4.创建自定义对象数组,必须提供无参构造函数。 366 同时数组有几个元素就调用几次"无参构造函数和析构函数"。 367 delete void* 指针可能会出错。没有调用析构函数 368 class print 369 { 370 private: 371 static int a; //在类中声明静态变量 372 public: 373 374 }; 375 int print::a = 0; //在类外初始化静态变量 376 377 5.静态成员变量: 378 1)在编译阶段就分配空间。对象还没有创建 379 2)必须在类中声明,在类外初始化(或定义)。 380 3)归同一个类的所有对象所有,共享同一个静态变量。在为对象分配的空间中不包含静态成员所占空间 381 4)有权限限制:private静态变量 和 public静态变量。 382 private静态变量;在类外不能访问和操作。 类外访问:print::a;"erro"; 类外操作:print::a = 6;"erro" 383 public静态变量;在类外可以访问和操作。 类外访问:print::a;"yes"; 类外操作:print::a = 6;"yes" 384 5)const 静态成员变量(const static int a = 8); 385 定义静态变量 const 静态成员变量时,最好在类的内部初始化。 386 387 "静态变量的调用:" 388 用类名加域作用符: print::a; 389 用对象名加取址符: class p1; p1.a; 390 391 6.静态成员函数:(主要是为了访问静态变量) 392 与静态变量一样,在没有创建对象前即可通过类名调用。 393 1).静态成员函数 只能访问 "静态成员变量",不能访问普通成员变量。 394 2).静态成员函数的使用规则和静态成员变量一样 395 3)静态成员函数也有使用权限。类外无法访问私有静态函数。 396 4)普通成员函数可访问"静态成员变量",也可访问"非静态成员变量" 397 398 399 400 7.单例模式:一个类只存在一个对象。 401 条件:1)构造函数、拷贝函数、析构函数私有化。 402 2)提供私有化静态成员变量指向一个对象 403 3)提供外部获得对象的静态成员函数。 404 class person 405 { 406 public: 407 void print_func(string content) 408 { 409 cout << "打印内容" << content << endl; 410 mcount++; 411 cout << "打印次数" << mcount << endl; 412 } 413 static person *getInstance(){ //静态成员函数 “在公共区域”,提供接口 414 return p1; 415 } 416 private: 417 person(){ //私有化构造函数 418 mcount = 0; 419 } 420 person(const person &){} //私有化拷贝函数 421 ~person(){} //私有化析构函数 422 private: 423 static person * p1; //私有化静态成员变量 424 int mcount; 425 }; 426 person *person::p = new person; (类外初始化静态变量)//私有化的静态成员变量指向一个对象 427 428 int main(void) 429 { 430 person *p1 = person::getInstance(); p1和p2的地址一样。 431 person *p2 = person::getInstance(); 因为他们为指向同一个对象。 432 433 p1->print_func("晚上好!!!"); 434 p2->print_func("加油,胜利就在眼前!!!"); 435 436 return 0; 437 } 438 439 8.C++ 成员变量和函数的存储:(成员变量和函数分开存储) 440 1)静态变量不在类对象中存储 441 2)函数不在类对象中存储(静态函数与非静态函数都是) 442 3)类对象中只存储 普通成员变量。 443 444 9.this 指针:(因为C++成员的存储特点。) 445 1)this指针永远指向当前对象。 446 2)"静态成员函数" 内部没有 this 指针,所以静态成员函数不能操作非静态成员变量。 447 "this 指针的使用:" 448 1)当形参与成员变量同名时,可以通过this指针区分。 449 2)在类的非静态成员函数中返回对象本身。可以使用:return *this; 450 class person 451 { 452 public: 453 person(int a, string name) 454 { 455 this->a = a; 456 this->name = name; 457 } 458 person func() 459 { 460 return *this; 461 } 462 463 public: 464 int a; 465 string name; 466 }; 467 468 10.空对象指针访问成员的不同情况:由于( this 指针的存在) 469 1)当不访问成员变量的时候是允许的。 470 2)当访问成员变量时,函数通过this指针寻找成员变量,此时就会报错。 471 472 class ARE 473 { 474 public: 475 void print(){ 476 cout << "hello world !!1" << endl; 477 } 478 void get_func(){ 479 cout << ma << endl; 480 } 481 482 public: 483 int ma; 484 }; 485 486 int main(void) 487 { 488 ARE *a1 = NULL; 489 a1->print(); "可以访问,因为此时不访问成员变量" 490 a1->get_func(); "不可以访问,它队成员变量进行了访问" 491 return 0; 492 } 493 494 10.const 对成员函数的修饰。(int func() const {});注意:const 要写在小括号后面,大括号前面。(===》int func(const ARE * const this )) 495 1)const 修饰 this 指针指向的内存区域。成员函数体内不可改变类中任何变量,除非变量前面用 mutable 进行修饰。 496 mutable 对成员变量的修饰 497 1)用它修饰后,任何情况下都可以对变量进行修改。 498 11.const 修饰对象(常对象)。(不允许修改对象的属性) 499 1)常对象可访问 const 或非 const 变量,不能修改变量。除非变量前用 mutable 修饰。 500 2)常对象只能调用 const 修饰的常函数。 501 502 503 12.友元函数。(全局函数和成员函数两种) 504 1).friend 关键字只出现在声明处。 505 2).其他类、类成员函数、全局函数都可声明为友元。 506 3)友元函数不是类的成员,不带this指针。 507 4)友元函数可访问对象的任意成员属性,包括私有属性。 508 509 注意:注意: 510 1)友元关系不能被继承 511 2)友元关系是单向的,类A是类B的友元,但类B不一定是类A的友元。 512 3)友元关系不具有传递性。类B是类A的友元,类C是类B的友元,但类C不一定是类A的友元。 513 514 #include <iostream> 515 #include <string> 516 using namespace std; 517 class COp2; "注意:这里必须对第二个类进行声明" 518 class Myp1 519 { 520 public: 521 void printf(COp2 &); "这个类中的函数对另一个的成员变量进行操作" 522 }; 523 524 class COp2 525 { 526 friend void print_func(COp2 &c1); "全局函数 友元函数的声明" 527 friend void Myp1::printf(COp2 &); "类成员函数 友元函数的声明。注意这里只能声明" 528 friend class Myp1; "友元类的声明,此类中的所有函数都可以对声明的类中的变量进行访问" 529 public : 530 COp2(int a, string name) 531 { 532 this->a = a; 533 this->name = name; 534 } 535 private: 536 int a; 537 string name; 538 }; 539 540 void Myp1::printf(COp2 & c1) "成员函数的 友元函数的实现"" 541 { 542 c1.a += 1; 543 cout << c1.a << endl; 544 cout << c1.name << endl; 545 } 546 547 void print_func(COp2 &c1) "全局函数的 友元函数的实现" 548 { 549 cout << c1.a << endl; 550 cout << c1.name << endl; 551 } 552 void test() 553 { 554 COp2 c1(30,"bei jing"); 555 Myp1 m1; 556 m1.printf(c1); 557 } 558 559 int main(void) 560 { 561 COp2 c1(30, "bei jing"); 562 Myp1 m1; 563 m1.printf(c1); 564 system("pause"); 565 return 0; 566 } 567 568 569 第五天C++==================================++++++++++++++++++++++++++++++++++++++++= 570 571 1.友元函数案例: 572 1)电视机的状态 573 574 2)遥控调试 575 576 577 2.数组案例: 578 1)分文件( int 类型的数组) 579 2) 580 581 3.操作符重载: 582 1)本质:"函数的调用"。 583 函数的参数取决于两个因素: 584 1)运算符是一元还是二元。 585 2)运算符被定义为全局函数还是成员函数。 586 587 2)运算符重载的限制: 588 1)使用运算符重载不能改变预算符的优先级,不能改变运算符的参数个数。 589 1)不能重载的运算符:"."; "::"; ".*"; "? :"; "sizeof". 590 2)除"="号外,基类中重载的运算符都将被派生类继承。 591 3)特殊运算符: 592 1)“=”; “[]”; “()”; “->”;操作符只能通过成员函数重载 593 2)“<<”; “>>”; 只能进行友元函数重载 594 3)不要重载 “&&”; “||” ; 操作符,因为无法实现短路原则。 595 4.运算符重载建议: 596 1)所有的一元运算符, 建议使用:成员函数 597 2)“=”;“[]”;“”;“->”; 必须使用成员函数 598 3)+=,-=,*=,&=,!=,%=,^=, 建议使用:成员函数 599 4)其他二元运算符 建议使用:非成员函数 600 5.重载"="号操作符的步骤 601 1)先释放旧内存 602 2)返回一个引用,实现链式编程。 603 604 考试天================================================================================== 605 1.断言函数; assert(终止进程,抛出异常) 头文件:<assert.h> 606 2. strcpy 返回值为目标内存的地址;是为了实现链式编程。 607 int n = strlen(strcpy(des,src)); 链式编程。 608 3. 609 610 611 C++第六天========================================++++++++++++++++++++++++++++++++++++++++++++++++++= 612 "运算符重载" 613 1.前置++;前置--; 614 2.后置++;后置--; 615 要创建一个临时变量接收计算时的那个变量,记住要把临时变量返回去。 616 3.== 和 != 号的重载。 617 618 4.继承: 619 1)C++中的继承方式(public, private, protected)会影响子类的对外访问属性。判断某一句话,能否被访问 620 (三看原则 ) 621 1)看调用语句,这句话写在子类的内部、还是外部 622 2)看子类如何从父类继承(public, private, protected) 623 3)看父类中的访问级别(public, private, protected); 624 625 2)继承的使用情况: 626 1) protected 继承,只能在家族内部使用,不能在类的外部使用 627 2)项目开发中,通常都是 public 继承。 628 629 5.继承的类型兼容原则: 630 1)子类对象可以当作父类对象使用 631 2)子类对象可以直接赋值给父类对象 632 3)子类对象可以直接初始化父类对象 633 4)父类指针可以直接指向子类对象 634 5)父类引用可以直接引用子类对象。 635 636 6.继承与组合混搭情况下,构造和析构函数调用原则 637 1) 先调用父类构造函数,再调用组合类构造函数,最后调用自己的构造函数 638 2)先调用自己的析构函数,再调用组合类析构函数,最后调用父类析构函数, 639 3)先构造的对象后释放 640 641 7.不可以被继承的函数 642 1)基类中的构造函数和析构函数 643 2)基类中重载的运算符 = 号函数 644 645 8.继承中同名成员函数和变量的问题: 646 1)如果子类和父类中有相同的函数和变量名;子类调用时默认调用自身的函数和变量。 647 2)如果想调用父类的函数和变量,需要加上 "类名和域作用域符",显式调用。 648 649 9.继承中的静态成员特性: 650 1)基类中定义的静态成员,可被所有派生类共享 651 2)"静态成员的访问权限和普通成员一样";遵守派生类的访问控制 652 3)与函数重载一样,如果子类中重新定义了同名的静态函数,基类的函数将被隐藏 653 4)静态成员函数不能是虚函数。 654 655 9.继承中的函数重载现象; 656 1)"如果重新定义了基类中的重载函数,"只要函数名相同,继承的基类中的重载函数将被隐藏。 657 想要调用必须使用域作用符显式调用。 658 659 9.多继承的概念: 660 1)多继承可能会出现二义性; 解决办法:显示的调用不同类中的同名属性或方法 661 2)多继承的相同数据重复继承。解决办法:虚继承:virtual 既保持一份数据,也不会产生二义性。 662 663 10.菱形继承解决办法: 664 加上 virtual ,用虚继承。 665 11.多态 :分为:静态多态(编译时多态)和动态多态(运行时多态)。 666 面向对象编程的设计思路:"开闭原则(对修改关闭,对扩展开放。)" 667 C++的多态性是通过虚函数来实现的。(虚函数允许子类重新定义父类成员,这种做法叫做重写或覆盖)。 668 12.向上类型转换 669 670 13.虚函数:函数前加 virtual 修饰 671 1)virtual 关键字只能修饰成员函数 672 2)构造函数不能为虚函数 673 3)创建一个虚成员函数,只需在函数声明前加上 virtual 关键字。 674 4)如果一个函数在基类中被声明为 virtual ,那么它在所有派生类中都是 virtual ;它派生类的函数前可以加也可以不加 virtual 675 关键字;推荐加。 676 677 纯虚函数:virtual int func() = 0; 678 1)当一个类中有纯虚函数时,这个类为抽象类;"同时注意:不能实例化一个抽象类,编译时会报错"。 679 2)当继承一个抽象类时,必须实现所有的纯虚函数,否则抽象类派生的类也是一个抽象类。 680 681 14.建立公共接口的目的: 682 1)将子类中公共的操作抽象出来。可以通过一个公共接口操作呢一组类。这个公共接口不需要事先实现,即创建一个公共类。 683 684 15.模板方法:就是通过创建一个公共借口来实现的。 685 686 687 688 "===========================================================================================" 689 一.多态的思想: 690 1.封装:突破C函数的概念,。。。用类做函数参数的时候,可以调用对象的属性和对象的方法 691 2.继承:A B代码可以复用,可以使用前人的代码 692 3.多态。可以使用未来的代码,架构不用改变 693 二. 多态成立的条件: 694 1.要有继承 695 2.要有虚函数重写 696 3.用父类指针或父类引用 指向子类对象 697 698 "===========================================================================================" 699 700 C++第七天========================================++++++++++++++++++++++++++++++++++++++++++++++++++= 701 702 1.虚析构函数的特点和目的: 703 1)通过父类指针,把所有子类对象的析构函数都执行一边。 704 2)通过父类指针,释放所有的子类资源 705 706 2.函数的重写、重载、重定义 707 1)重载: 708 必须在同一个类中 709 重载是在编译期间根据 参数类型和个数决定函数调用的 710 子类无法重载父类的函数,父类同名函数将被覆盖 711 712 2)重写: 713 必须发生在父类与子类之间。(同时分为两类) 714 并且父类与子类的函数有完全相同的原型。 715 1)virtual 具有这个关键字的,发生重写时会产生多态。 716 2)无这个关键字的,叫重定义。 717 3)重定义:(非虚函数重写叫重定义) 718 父类与子类的有完全相同的函数原型,而且无 virtual 关键字修饰,此时叫做函数重定义。 719 720 3.子类中的 vptr 指针的分步初始化 721 1)当创建子类对象时,如果它具有 vptr 指针, 那么此时是分步初始化的。 722 2)当执行父类的构造函数时,子类的 vptr 指针会指向父类的虚函数表 723 当父类的构造函数执行完毕后,会把子类的 vptr 指针指向子类的虚函数表 724 3)结论:子类的vptr指针是分步初始化的。它在程序运行时会进行寻址操作,效率会降低。不建议将每个成员函数都声明为虚函数。 725 726 4.纯虚析构函数: 727 728 5.多态的案例: 729 1)思路: 730 1.定义一套自己的数据接口,打包放在结构体中,数据接口使用函数指针。 731 2.初始化模块接口。(将自己定义的接口与生产商实现的函数联系在一起。) 732 3. 733 734 735 6.函数指针 736 737 738 739 C++第八天================================================++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 740 741 1.C++的模板(用于实现泛型编程) 742 1)模板关键字: template<class T,class T1>或 template<typename T, typename T1> T,T1代表数据类型; 743 2)定义的模板关键字当前行下一个函数或类有用。 744 3)分为 函数模板 和 类模板; 745 4)一旦声明了多个类型 T ,不管用或不用,都必须给它指定类型。 746 747 2.函数模板的编译机制:(原理) 748 1)函数模板具有自动推导功能 749 2)编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。 750 3)函数模板通过具体的类型产生不同的函数。 751 752 3.模板函数与普通函数的区别 753 1)函数模板不允许自动类型转换,必须严格匹配。 754 2)普通函数能自动进行类型转换 755 template<class T> 756 T ptint(T a, T b ){ } 757 758 int show(int a,int b){ } 759 void main(){ int a = 6; char b = 'c'; print(a,b);"error",两个变量类型必须相同,不允许进行自动转换 760 show(a, b);"YES" “普通函数能自动进行类型转换”}; 761 762 4.函数模板和普通函数一起调用规则 763 1)函数模板可以向普通函数一样被重载 764 2)如果函数模板与普通函数一样,C++编译器优先调用普通函数。 765 例如: template<class T> 766 void print(T a, T b ){ } 767 void print(int a, int b){ } 768 void main(){ int a = 4; int b = 9; print(a, b); "此时优先调用普通函数"} 769 3)如果函数模板能产生更好的匹配,则优先调用函数模板。 770 例如:template<class T,class T2> 771 void print(T a, T2 b ){ } 772 void print(int a, int b){ } 773 void main(){ int a = 4; char b = 'u'; print(a, b); "此时优先调用函数模板"} 774 4)可以通过空模板实参列表的语法限定编译器只能调用模板函数 775 例如:(此目录第二个函数例子) 776 void main(){ int a = 4; int b = 6; print<>(a, b); } 777 4.类模板: 778 1)只能显示的指定类型,不具有自动推导功能。 779 2)类模板做函数形参(必须显示指定模板类型) 780 3) 781 782 5.类模板与函数模板(有自动推导功能)的混合使用案例,有奇效。 783 template <class T1,class T2> 784 class Person 785 { 786 public: 787 Person(T1 name, T2 age) 788 { 789 this->mName = name; 790 this->mAge = age; 791 } 792 public: 793 T1 mName; 794 T2 mAge; 795 }; 796 //类模板做函数参数(指定类型) 797 void Dobusiness(Person<string, int> &p1) 798 { 799 cout << "name:" << p1.mName << " age:" << p1.mAge << endl; 800 } 801 //类模板与函数模板混合使用(使用函数模板的自动推导功能) 802 template <class T1, class T2> 803 void Dobusiness02(Person<T1, T2> &p1) 804 { 805 cout << "name:" << p1.mName << " age:" << p1.mAge << endl; 806 807 } 808 void test04() 809 { 810 Person<string, int > p1("jonh", 30); 811 Dobusiness(p1); 812 813 Person<string, int > p2("jonheqeq", 303); 814 Dobusiness02(p2); 815 } 816 817 6.类模板派生普通类和类模板类: 818 1)继承的话,应该继承一个具体的类,因为一个具体的类,编译器才知道分配多大的内存。 819 2)如果想继承类模板,应该给继承过程中的类模板显示指定类型。 820 例如(类模板派生普通类) 821 template <class T> 822 class MyClass{ private: "父类模板" 823 T age; }; 824 class SubClass : public Mycalss<int>{ "普通类,这里继承时,必须对类模板显示指定类型,编译器才知道给父类分配多大的内存" 825 public: 826 int a; 827 }; 828 例如(类模板派生子类模板) 829 template <class T> 830 class MyClass{ private: "父类模板" 831 T age; }; 832 template <class T> 833 class SubClass : public Mycalss<T>{ "子类类模板,这里继承时,可以用子类的类型指定父类的模板" 834 public: 835 T a; 836 }; 837 838 839 7.类模板分文件编写 840 C++文件的编译过程:先每个文件独立编译,然后由链接器将编译好的文件链接在一起(这一步就是寻找各种调用函数,和头文件的定义内容) 841 需要引入 .cpp 文件,原因: 842 1)C++编译机制的原因;(分文件编译导致的) 843 2)二次编译有关 844 解决办法文件,将他们放入: 845 类模板的声明与定义要放在一个文件中编写。创建一个.hpp将他们放入 846 847 848 "C++的类型转换" 849 850 8.静态类型转换:static_cast<>; 851 1)用于类层次结构中基类和派生类之间"指针或引用"的转换 852 1)上行转换:把派生类的指针或引用转换成基类的是安全的 Animal *ani = static_cast<Animal*>( dog ) ; 853 2)下行转换:把基类的指针或引用转换成派生类的是不安全的,因为没有动态类型检查。 Dog *dog = static_cast<Dog *>(ani); 854 3)总结一句就是:可以把大的转成小的; 不能把小的转成大的。 855 856 2)当两个类之间无继承关系时,不可以进行转换。 857 如:Teacher *t1 = static_cast<Teacher *>(dog); "error,因为两个类之间无关系" 858 859 3)基础数据类型之间的转换: 860 1)可以把高精度的类型转换成低精度的;double da; int a = static_cast<int>(da); 861 2)当把低 862 精度的类型转换成高精度时,安全性需要程序员保证。(原理和上一样) double da = static_cast<double>(a); 863 864 9.动态类型转换:dynamic_cast<>(类与类之间的转换) 865 dynamic_cast<> 具有类型检查功能,比 static_cast<>类型转换安全。 866 1)只能转换具有父子关系的"指针或引用". 867 上行转换:只能将子类指针转换成父类指针(可以大转小) 868 下行转换:不能将父类指针转换成子类指针(不能小转大); 869 例如: parent *parent = dynamic_cast<parent*>son; 870 871 10.数组模板: 872 "1)自定义类型做元素构建数组时,必须提供无参构造函数。" 873 874 875 C++第九天================================================++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 876 877 1.常量转换:(const_cast<>)用来修改类型的 const 属性。 878 1)常量指针、引用 被转换成 非常量指针、引用,并且仍然指向原来的对象()互相转换,将非常量转换为常量。 879 例如u: 880 2)不能直接对非指针、非引用的变量直接使用 const_cast<> 操作符去移除它的 const 属性。 881 882 2.重新解释转换或强势类型转换。reinterpret_cast<>(不安全的转换,各种类型之间的强转) 883 884 3.异常的处理(需要了解 C 的异常处理 和C++的处理) 885 C的异常处理的缺陷。和特点 886 887 4.C++的异常特点 888 1)异常不能被忽略,而且异常可以跨函数,异常并不是简单的 int 型数字,最好有明确的意义。 889 2)C++提供的异常机制,具有跨函数和不可忽略的特点。 890 3)异常捕获的严格类型匹配。 891 892 893 5.栈解旋:异常被抛出后,从进入 try 起,到异常处理完毕,这期间在栈上创建的所有对象都将会自动析构;析构的顺序与创建的顺序相反。 894 895 6.异常接口声明: 896 1)可以在函数前声明抛出的异常类型,如果不声明,表示可以抛出各种类型的异常。 897 例如: void func() throw(int , double, char){ }可以抛出三种类型的异常。 898 void func() throw(){ }不可以抛出任何类型的异常。 899 void func() { } 可以抛出任何类型的异常。 900 2)"但是:" 901 C++的异常规范在不同编译器下可能执行效果会不同。(编译器的不同会导致结果不同) 902 VS下会忽略C++的异常规范,但是会发出警告,程序照常执行; QT会报错,终止程序运行。 903 通常情况下:如果一个函数抛出了它的异常接口声明所不允许抛出的类型,unexcepted 函数将被调用,该函数默认调用 terminate 函数中断程序。 904 905 7.异常变量的声明周期 906 1)异常也可以抛出一个类的对象。此时可以使用纯虚函数做接口,声明每种不同的异常对象(减少代码量)。 907 例如: class BaseException{ 908 public: 909 virtual void print_exception() = 0; 910 }; 911 class TargetException : public BaseException{ 912 public: 913 virtual void print_exception(){ 914 cout<<"目标 空间空指针异常" <<endl; 915 } 916 }; 917 class DestException : public BaseException{ 918 public: 919 virtual void print_Exception(){ 920 cout<<"源空间的空指针异常" << endl; 921 } 922 }; 923 void CopyString(char *dest, const char *source){ 924 if(dest == NULL){ 925 throw TargetException(); 926 } 927 if(source == NULL){ 928 throw DestException(); 929 } 930 memcpy(TargetException,DestException,strlen(source)+1 ); 931 } 932 int main(){ 933 const char *source = "nnakew"; 934 char dest[1024] = {0}; 935 try{ 936 CopyString(dest, source); 937 }catch(BaseException& ex ) 938 { 939 ex.print_exception(); 940 } 941 return 0; 942 } 943 944 945 946 8.C++标准异常类:系统头文件 <exception> 947 948 9.创建自己的异常类: 949 引入系统头文件,继承系统的标准 出错类;重载父类的 what 函数和虚析构函数。 950 951 10.标准输入输出操作 952 cin 是由缓冲区获取文件到 目标内存或变量,若缓冲区没有内容时,才需要从键盘上输入 953 cout 是有缓冲区输出文件到屏幕的,若缓冲区没有满时,或强制释放,它是不会在屏幕上输出内容的。 954 endl 的作用是刷新缓冲区,将内容输出到屏幕或文件里。 955 956 10.格式化输入输出操作 957 958 959 960 C++第十天(STL课程阶段)================================================++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 961 962 1.STL六大组件 963 964 965 2.STL三大组件:容器、算法、迭代器 966 967 每一个容器都有自己的迭代器,迭代器用来遍历容器中的元素。 968 遍历:不重复的进行访问每个元素。 969 970 3.容器,迭代器,算法的初步使用。 971 1)先引入所需要的容器头文件和算法头文件 972 973 void Myprint( int val){ 算法中的回调函数,需要自己写, 974 cout<< val <<" "; 内容为自己所想打印的内容 975 } 976 void test(){ 977 vector<int > V; 978 for (int i = 0; i < 5; i++) 979 { 980 V.push_back(i+ 10); 981 } 982 vector<int>::iterator pBegin = V.begin(); 983 vector<int>::iterator pEnd = V.end(); 984 985 //直接使用迭代器进行容器的遍历 986 while (pBegin != pEnd) 987 { 988 cout << *pBegin << " "; 989 pBegin++; 990 } 991 cout << endl; 992 993 //模拟算法的最后一个回调函数的使用 994 pBegin = V.begin(); 995 while (pBegin != pEnd) 996 { 997 Myprint(*pBegin); 998 pBegin++; 999 } 1000 cout << endl; 1001 1002 //使用算法遍历容器,并且打印出来。注意:算法的最后一个参数为回调函数,需要自己写(所想要打印的内从容) 1003 for_each(V.begin(), V.end(), Myprint); 1004 cout << endl; 1005 } 1006 }; 1007 1008 2)容器的分类:序列式容器和关联式容器 1009 序列式容器:容器元素在容器中的位置由元素进入容器的时间和地点来决定。如:(vector,deque , list, stack, queue ) 1010 关联式容器:容器具有自己的规则,元素在容器中的位置由容器的规则来定。如(树状容器: set/mutilset; map/mutilmap ) 1011 3)每个容器都有自己的迭代器,由容器自己提供。 1012 1013 3.string 容器: 1014 1)从 const char * 到 string 有隐式转换;反之则没有。 1015 1016 3.下标操作符: [] 和 at()方法。 1017 两个的区别在于:1)[] 越界程序会直接挂掉, 2)at()方法越界程序会提供错误信息。 1018 1019 4.vector 容器: 1020 1)连续的内存空间 1021 2)单口 1022 3)会实现内存空间动态增长(当已有内存占满时);申请更大的空间,原来的空间析构,原来的迭代器会失效。 1023 4)它的内存空间不会随着 clear()方法清除数据而消失的,只有当容器生命周期结束时,它的容量才会释放; 1024 "所以当想在生命周期存在时减少或增大容器内存,可以使用 swap() 方法。"(如下) 1025 void test04(){ 1026 1027 vector<int> v; 1028 for (int i = 0; i < 100000; i++){ 1029 v.push_back(i); 1030 } 1031 1032 cout << "capacity:" << v.capacity() << endl; 1033 cout << "size:" << v.size() << endl; 1034 1035 v.resize(3); 1036 cout << "capacity:" << v.capacity() << endl; "注意容量与长度不一样," 1037 cout << "size:" << v.size() << endl; 1038 1039 //收缩内存 1040 //vector<int>(v).swap(v); 1041 1042 vector<int>(v).swap(v); //匿名对象 1043 1044 cout << "capacity:" << v.capacity() << endl; 1045 cout << "size:" << v.size() << endl; 1046 } 1047 5)reserve的使用:"预留空间",在此空间还没有初始化时是不允许访问。可以通过push_back()插入元素后,进行访问。 1048 6)resize的使用:开辟空间并且初始化,它的空间申请完后是可以访问的。 1049 重新指定容器的长度为num,若容器变长,则默认值或指定值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除,"只是长度变短,但是容器的容量没有变化,"。 1050 7)只要容器的容量产生变化,原来的迭代器就会失效。(如删除元素之后,原来的迭代器就不能用了)。 1051 1052 5.迭代器的注意事项: 1053 1)"(当内存由于动态增长,更换地址后,原来的迭代器就失效了,不可以再使用)。" 1054 2)只要能遍历容器中所有元素的,都是容器认可的迭代器。 1055 3)每个容器的迭代器不一样,有自己的独立迭代器。 1056 4) const_iterator 只读 1057 reverse_iterator 逆序迭代器 1058 iterator 最普通的正向迭代器 1059 5) 1060 6.迭代器的种类:5种。 1061 1)每个容器只提供一种 1062 2)都是由对应的容器自己提供的迭代器。(因为每个容器的实现机制不一样)。 1063 3)输入迭代器,输出迭代器,前向迭代器,双向迭代器,随机访问迭代器 1064 1065 1066 6.vector 申请和释放空间的注意: 1067 尽量不要频繁申请和释放空间(太浪费时间);可以先预测一下所用空间的大小,直接用reserve()申请足够大的空间,在逐步初始化。 1068 1069 1070 7.随机迭代器:(vector容器支持随机访问) 1071 1072 8.deque 容器: 1073 1)是一种双向开口,动态以分段连续内存空间组合而成,随时可以增加一块新内存的容量器。 1074 2)可以在首尾两端分别作元素的删除和插入操作。 1075 3) 1076 1077 1078 C++第十一天(STL课程阶段)================================================++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1079 1080 1.栈容器(stack) :元素先进后出,单口容器 1081 1)不提供迭代器;没有遍历功能 1082 2)以其他的容器作为底层,它相当于提供接口 1083 3)只有栈顶元素才可以被外界取用。所以想遍历容器,只能从栈顶开始,取出一个元素,就删除它,当遍历完后,容器也就没有元素了。 1084 1085 2.队列容器(queue):元素先进先出 1086 1)没有迭代器。不支持随机访问 1087 2)两个出口,一个只进,一个只出。 1088 3)和 stack 栈容器一样,遍历完后,容器的元素也被删除空。 1089 1090 3.( stack 和 queue 容器叫受限的线性表) 1091 1092 3.链表容器(list):双向循环链表 1093 1)采用动态存储分配,不会造成内存浪费和溢出。 1094 2)元素的插入和删除十分方便,修改指针即可。 1095 3)list容器提供自己的排序算法; 1096 4)list容器的排序(重点在回调函数的使用) 1097 5)swap()既可以交换两个链表的数据,也可以动态的伸缩内存。 1098 1099 1100 1101 4.set容器(红黑树,平衡二叉树的一种) 1102 1)它的元素既是键值又是实值,而且所有的元素都会自动排序。 1103 2)不允许两个元素有相同的键值。 1104 3)不允许通过迭代器修改元素的值。 1105 4)与list有某些相同的性质,当对容器中的元素进行插入和删除操作时,操作之前的所有迭代器,在完成操作完成后都是有效的,除了被删除的那个元素的迭代器 1106 1107 5.multiset容器: 1108 1)特性和set一样,唯一区别在于它允许键值重复。 1109 1110 6.算法的默认排序原则:都是由小到大;如果想从大到小,就需要自己建立一个回调函数 1111 1112 7.对组( pair ): 将一对值组合成一个值,这一对值可以具有不同的数据类型。两个值可以分别用对组的公有属性 first 和 second 访问。 1113 创建对组的两种方式:1)pair<string ,int> pair1("xiaobai",20); cout<< pair1.first <<endl; cout<< pair1.second <<endl; 1114 2)pair<string, int> pair2 = make_pair("xiaohong",23); cout<< pair2.first <<endl; cout<< pair2.second <<endl; 1115 3)pair<string, int> pair3 = pair2; cout<< pair3.first <<endl; cout<< pair3.second <<endl; 1116 1117 8.map 容器:(它的所有元素都是一个对组) 1118 1)键值对;键值不可以相同,实值可以相同。所有的元素都会根据键值自动排序。 1119 2)插入元素的四种方式:map<int , int> m; 1120 m.insert(pair<int, int>(1, 2)); 1121 m.insert(make_pair(2, 2)); 1122 m.insert(map<int, int>::value_type(3, 2)); 1123 m[4] = 4; 1124 1125 3)如果通过[] 访问一个不存在的key值,那么编译器会创建一个,实值为默认值。 1126 4)map的迭代器与普通容器不同。是一个对组迭代器。可以通过迭代器修改实值的值,不可以修改键值的值。 1127 5)map的迭代器与list迭代器有某些相同的性质,对容器元素进行插入和删除时,操作之前的所有迭代器在操作完成之后不会失效,当然那个被删除的元素迭代器除外。 1128 6)指定map 的排序规则:(因为它的元素为对组,所以排序规则需要自己写函数确定) 1129 1130 9.multimap容器: 1131 与map 容器操作类似,唯一不同在于它的键值可以相同。 1132 1133 10.如何判断容器支持随机访问(或提供随机迭代器): 1134 只需要看容器提供的迭代器能否 +2,+3 1135 vector<int>::iterator it = v.begin(); it = it+3;(可以,提供随机迭代器) 1136 queue<int>::iterator it = q.begin(); it = it+2;(不可以向后跳跃,不提供随机迭代器)。 1137 1138 11.STL中,所有的拷贝都是值寓意,所提供的内容必须是可以拷贝的。 1139 1)此时就涉及到深拷贝和浅拷贝的问题;当由指针存在,而且它指向堆内存时,用容器提供的拷贝只会复制指针的指向,并没有拷贝到指针所指向的数据。 1140 当生命周期结束,进行空间析构时,就会出现同一块内存二次析构,程序挂掉。 1141 2)必须自己重载一个深拷贝函数(和类的函数重载一样;(一般都是:一个拷贝函数和一个重载=号操作函数)。 1142 1143 12.STL使用 的时机 1144 1145 C++第十二天(STL课程阶段)================================================++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1146 1147 1.容器中的 find()函数默认区分大小写,而且返回值为迭代器. 1148 1149 2.仿函数:重载函数调用操作符的类,其对象成为函数对象,也叫仿函数。 1150 1)仿函数是一个类,不是一个函数。重载了() 1151 2)仿函数重载了()操作符,使得函数对象可以像函数一样使用。 1152 3)重载的operator()需要一个参数的,这样的类对象称为 "一元仿函数"; 依次类推。 1153 1154 3.预定义函数对象:(实现了数据类型与算法的分离。) 1155 1)plus<int> p; 预定义好的函数对象,能实现不同类型数据的 + 运算。 1156 2) 1157 1158 3.函数没有类型,不能定义变量。函数对象有类型,因为他是一个类。 1159 1160 4.函数对象与普通函数的区别:(02) 1161 1)函数对象超出了普通函数的概念,可以保存函数的调用状态 1162 2)函数对象可以做参数和返回值 1163 1164 5.谓词: 1165 1)普通函数或重载的operator()"返回值为bool 类型"的函数对象(仿函数)。 1166 2)一个参数的叫一元谓词,以此类推。 1167 6.兰博打函数表达式: vector<int> v1; 1168 for_each(v1.begin(), v1.end(), [](int val){ cout<< val<<" "; }); 1169 for_each(v1.begin(), v1.end(), [](int val)->void{ cout<< val<<" "; }); 1170 1171 7.内建函数对象 1172 /* 1173 template<class T> T plus<T>//加法仿函数 1174 template<class T> T minus<T>//减法仿函数 1175 template<class T> T multiplies<T>//乘法仿函数 1176 template<class T> T divides<T>//除法仿函数 1177 template<class T> T modulus<T>//取模仿函数 1178 template<class T> T negate<T>//取反仿函数 1179 */ 1180 7.函数对象适配器: 1181 1). 让自己编写的函数对象继承基类,如果是一元函数对象需要继承unary_function, 1182 如果是二元函数对象继承binary_function 1183 2). 函数对象operator()函数后增加 const 1184 3). 使用bind2nd bind1st(将一个二元函数对象转变为一元函数对象), 1185 区别为: bind1st: 将参数绑定为函数对象的第一个参数 bind2nd: 将参数绑定为函数的第二个参数。 1186 1187 4)for_each():遍历打印容器内容。返回值为最后一个函数对象的类型。 1188 1189 6)取反适配器 1190 find_if():查找容器中有无所寻找的内容,返回值为查找类型的迭代器。 1191 对一元函数对象、谓词取反用 not1; 对二元函数对象、谓词取反用 not2; 1192 1193 7)给普通函数绑定参数(需要:先把一个普通的函数指针适配成函数对象) 1194 ptr_fun函数指针适配器:把一个普通函数指针适配为函数对象。 1195 void Myprint(int val1, int val2){ cout<< val1 + val2 <<" "; } 1196 void main() { for_each(v1.begin(), v1.end(), bind2nd(ptr_fun(Myprint), 200)); } 1197 8)成员函数适配器(mem_fun_ref:容器中存放的为对象实体。)(mem_fun:容器中存放的为对象指针。) 1198 vector<person> v1; person p1("aaa", 10); v1.push_back(p1); 1199 for_each(v1.begin(), v1.end(), mem_fun_ref(&person::showperosn)); 1200 vector<person*> v1; v1.push_back(&p1); 1201 for_each(v1.begin(), v1.end(), mem_fun(&person::showperosn)); 1202 1203 1204 8.算法: 1205 遍历算法: 1206 1)for_each算法:第三个参数,接受的函数类型。 1207 2)transform; 1208 1209 查找算法 1210 1)find(查找元素或对象) 与 find_if(查找指针所指向的内容) 1211 2)adjacent_find查找相邻重复元素 1212 3)binary_search(二分查找法),需要查找的容器元素有序排列 1213 4)count 与 count_if (统计容器中元素的个数) 1214 1215 排序算法 1216 1)合并两个有序序列(merge)两个序列必须是有序的。且顺序应该一样(或从小大,或从大到小,两种方式参数有点区别,默认从小到大,从大到小需要额外添加函数对象) 1217 2) sort算法(条件:容器必须支持随机访问) 1218 3)random_shuffle(打乱容器中的顺序) 1219 4)reverse (反转) 1220 5) 1221 拷贝和替换算法 1222 1)copy (拷贝) 1223 2)replace(替换)把所有要替换的值都替换成目标值。 1224 3)replace_if 1225 4) swap 1226 1227 算数生成算法 1228 1)accumulate (累加容器中元素) 1229 2)fill(填充) 1230 3) 1231 集合算法 1232 1)set_intersection (求交集) 注意它的返回值。 1233 2)set_union (求并集) 1234 3)set_difference (差集); 1235 1236 1237 C++第十三天(数据结构、算法课程阶段)================================================++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1238 1.线性表:分类: 1239 1240 1)线性表顺序存储: 1241 1242 2)线性表线性存储: 1243 1)单向链表 1244 2)单向链表(企业链表) 1245 3)循环链表(企业链表单向) 1246 4)解决约瑟夫问题(用单项循环链表) 1247 5)双向链表 1248 6)受限的线性表 1249 1)栈的顺序存储( stack ) 1250 2)栈的链式存储( stack ) 1251 3)队列的顺序存储( queue ) 1252 4)队列的链式存储( queue ) 1253 5)栈的应用(1.就近匹配,用来测试编码的括号使用规范) 1254 6)栈的应用(2.中缀表达式转后缀表达式) 1255 7)(计算机)基于后缀表达式的计算; 1256 1257 1258 3.树的特点: 1259 1)非线性结构,有 1 个直接前驱,但可能有多个直接后继(1 :n); 1260 2)树的定义具有递归性,树中还有树。 1261 3)树可以为空,即节点为0。 1262 1263 4.树的一些专业名称 1264 1)节点的度:节点挂接的子树数(即一个节点有几个直接后继就有几度)。 1265 2)节点的层次:即根到该节点的层数(根节点算一层) 1266 3)树的度:所有 节点的度 中的最大值 1267 4)树的深度(或高度):指所有节点中的最大层数。 1268 5)节点数:一棵树中的所有节点个数(包括根节点)。 1269 6)叶子节点:即终端节点,没有后继。 1270 1271 5 二叉树 1272 1)二叉树的基本特征:(1 :2) 1273 每个节点最多只有两个子树 1274 左子树和右子树的位置不能颠倒 1275 2)二叉树的性质: 1276 1)在二叉树的第 i 层上,最多只有2^(i-1)个节点。(i > 0) 1277 2)深度为 k 的二叉树,最多只有 2^k -1 个节点。(k>0) 1278 3)对于任何一个二叉树,若度为2 的节点有n2个,则它的叶子数必为 n2+1个。 1279 4)满二叉树:深度为K,有 2^K-1 个节点。特点是:每层都充满了节点。 1280 6.完全二叉树: 1281 1)除最后一层外,每层的节点数都达到了最大值,且最后一层的节点尽力靠左。 1282 2)对于完全二叉树:若从上至下,从左至右编号,则编号为i的节点,其左孩子编号为 2i, 右孩子编号为 2i+1, 双亲的编号为 i/2;(i等于1时,为根除外)。 1283 1)左孩子右兄弟:可以将一颗多叉树转变为二叉树 1284 2)递归遍历(周游)二叉树 1285 3)非递归遍历二叉树(用栈的方法) 1286 4)遍历的三种方法: 1287 1)先序:先根再左再右 1288 2)中序:先左再根再右 1289 3)后序:先左再右再根 1290 5) 1291 1)已知二叉树的"先序"遍历和"后序"遍历序列"不能"唯一地确定这这棵树。 1292 2)已知二叉树的"先序"遍历和"中序"遍历"可以"唯一地确定这这棵树。 1293 1)先根据先序找到根节点,在根据中序确定根节点左右两边的节点 1294 2)再根据先序找到下一个根节点,再根据中序找到根节点左右两边的节点。 1295 3)"中序"、"后序"可以确定一棵树 1296 4)总体结论为:带"中序"的都可以确定一棵树,反之则确定不了 1297 1298 6)三种遍历的特点: 1299 1)先序遍历:可以确定每棵树的根节点。(特点: 它的输出序列中:第一个数为树的总根节点;剩下的子根节点,和中序遍历可以确定) 1300 2)中序遍历:可以确定根节点的左右子树。(它的特点为:输出序列中,根节点两边的为它的左右子树,和先序遍历配合可以确定一棵树) 1301 3)后续遍历:(它的特点;输出序列中,最后一个数为为树的总根节点。) 1302 1303 C++第十四天(数据结构、算法课程阶段)================================================++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1304 1305 1. 求二叉树叶子节点的数目(用全局变量写一遍) 1306 可以用静态变量和全局变量,还有形参参数。 1307 2. 求二叉树的高度(深度) 1308 1309 3.拷贝二叉树: 1310 1311 4.释放二叉树的节点。 1312 1313 5.井号法创建树。 1314 1315 6.排序算法 1316 1)新型冒泡排序(加标识量) 1317 2)新型选择排序(加标识量) 1318 3)插入排序: 1319 1.构建有序和无序序列,2.无序序列插入到有序序列中。 1320 2.应用场景:序列基本有序,元素较少。 1321 3.两步操作:元素拿出来,符合条件的元素后移。 1322 4)希尔排序: 1323 要点:增量:(len / 3 + 1);进行分组 1324 1325 缺点: 排序不稳定,相同大小的几个元素,排列完后前后位置可能会发生变化。 1326 2, 3, 4, 5, 5, 6, 5; 这几个5排序完后,可能与现在的前后顺序不同。 1327 1328 5)快排: 1329 要点:基准数(选取) 1330 一轮遍历下来,基准数的插入位置左边部分都比他小,右边部分都比它大。 1331 1332 6)归并排序:(将两个有序序列合并成一个有序序列) 1333 要点:1)先分组,将数据最后分成一个元素为一组的有序数列 1334 2)再两两合并排序; 1335 3)需要一个与本来元素相同大小的辅助空间。 1336 1337 7)堆排序: 1338 大顶堆:所有的根节点元素都比子节点两个元素大(完全二叉树)。 1339 小顶堆:………都比子节点元素小。 1340 1)先找出最后一个子根节点(len/2-1: len为数据的个数 。) 1341 2)从这个子根节点开始对堆进行数·初始化,形成(大顶堆或小顶堆) 1342 3)对初始化后的堆进行从第一个节点对最后一个节点交换(两两对应),每次交换完后,再对堆进行调整,(因为堆的结构可能发生变化)。 1343 1344 8)等号= 对排序效率的影响。 1345 1346 "C++实现链表:" 1347 1)老江的链表,宝哥的链表;有头节点,无头节点链表; 1348 2)与C做对比。 1349 1350 考试: 1351 一. 类中三种变量的初始化: 1352 1.static 静态成员变量时: (正确写法:在类外初始化:类型 类名::成员名) 1353 2.const 成员初始化; (正规写法:在类内用初始化列表初始化) 1354 3.const static 成员初始化: (正确写法:本地初始化;在类内定义的地方初始化) 1355 class person{ 1356 public: 1357 person(int num = 0) : Age(10) "初始化列表" 1358 { } 1359 void show() 1360 { 1361 cout<<"Age:"<<Age<<" ID"<<ID<<" size"<<size<<endl; 1362 } 1363 private: 1364 static int ID; "在类外初始化" 1365 const int Age; "初始化列表" 1366 const static int size = 100; "在本地初始化:即定义的地方" 1367 }; 1368 int person::ID = 30; 1369 1370 二.容器打印的三种方式 1371 1.回调函数 1372 2.回调对象 1373 3.兰博打表达式 1374 "=================================" 1375 4.C++11 的表达式: 1376 for(int val : arr){ "其中arr是一个数组。V是一个vector容器" 1377 v.push_back(val); 1378 } 1379 5.STL 求和表达式的第三个参数:累加初始值,容器的所有元素的和加上这个值为最后的求出值。 1380 1381 6.谓词;count_if() 的使用。 1382 7.绑定适配器: 1383 1384 三.用栈(两个)实现队列 1385 1.可以在一个类里面包含两个栈,用来实现队列的方式。当用这个类定义对象时,他就是一个队列。 1386 2.有意思;但是没想出来,两种方法做一遍,存博客 1387 1388 四.用归并的思想合并链表 1389 1.
五.迭代器的理解:
1.erase()函数的返回值,它的迭代器在循环遍历中的奇特之处;
2.循环遍历,it++放置不同的位置;
1)在正常的for循环位置
1)符合条件时erase()容器中的某个元素,但是没有接返回值
2)符合条件时erase()容器中的某个元素,接了返回值;
3)符合条件时,接了返回值,同时在下面接着 it--;看容器首位元素符合条件和不符合条件的两种情况。
2)不放在正常位置
1)放在不符合条件的情况下it++;
C++知识点整理
1.引用与指针有什么区别?
1)引用必须被初始化,指针不必。(const变量也必须在一开始定义的时候就完成初始化)
2)引用初始化以后不能被改变,指针可以改变所指的对象。(也即引用初始化完成之后,就永远指向初始化时指定的对象;但指针可以动态修改指向,使之指向其他对象.)
3)不存在指向空值的引用,但是存在指向空值的指针。
2.描述一下extern的用法
参见博文:http://www.cnblogs.com/yyxt/p/3891712.html
3.对于一个频繁使用的短小函数,在C语言中应用什么实现,在C++中应用什么实现?
C用宏定义,C++用inline;
在C++中要尽量使用inline函数代替宏定义,具体分析参见博文:http://www.cnblogs.com/yyxt/p/4799898.html之 条款02;
4. 当一个类X 中没有生命任何非静态成员变量与成员函数,这时sizeof(X)的值是多少,请解释一下?
对于C++中的一个空类
- classX { };
事实上并不是空的,sizeof(X)并不等于0,一般的结果是1。每个X的对象都有一个隐晦的1bytes,是被编译器安插进去的一个char,这样可以使得这个class的两个objects在内存中配置独一无二的地址。
当X作为另一个类的成员时,如:
- classA
- {
- public:
- X x;
- int a;
- };
由于X占一个字节,int占4个字节,再加上编译器的alignment调整,sizeof(Y)=8。
但是当一个类继承X时:
- classY :public X
- {
- public:
- int a;
- };
这时大部分编译器对于sizeof(Y)的结果是4,而不是8。这就是所谓的空白基类最优化在(empty baseoptimization-EBO或emptybaseclassopimization-EBCO)。在空基类被继承后由于没有任何数据成员,所以子类优化掉基类所占的1byte。EBO并不是c++标准所规定必须的,但是大部分编译器都会这么做.
5. 数组和指针有什么区别?
(1)数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。
(2) 修改内容上的区别:
- char a[] = “hello”;
- a[0] = ‘X’;
- char *p = “world”; // 注意p 指向常量字符串
- p[0] = ‘X’; // 编译器不能发现该错误,运行时错误
(3) 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),vp 为指针得到的是一个指针变量的字节数4bytes,而不是p 所指的内存容量。
(4)当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针
6. strcpy()和memcpy()的区别?
strcpy和memcpy都是标准C库函数,它们有下面的特点。
strcpy提供了字符串的复制。即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符。
已知strcpy函数的原型是:char* strcpy(char* dest, const char* src);
memcpy提供了一般内存的复制。即memcpy对于需要复制的内容没有限制,因此用途更广。
void *memcpy( void *dest, const void *src, size_t count );
strcpy和memcpy主要有以下3方面的区别:
1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
7. 总结static的应用和作用?
(1)函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
(2)在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
(3)在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
(4)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
(5)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
更多参见博文: http://www.cnblogs.com/yyxt/p/4011893.html
8. 总结const的应用和作用?
(1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
9. 结构(struct)和联合(union)的区别?
1) 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合转只存放了一个被选中的成员, 而结构的所有成员都存在。
2) 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的.
10. 类(class)与结构(struct)的区别?
(1)默认的继承访问权限: struct是public的,class是private的;
(2)class是引用类型,struct是值类型;
(3)class可以继承类、接口和被继承,struct只能继承接口,不能被继承;
(4)class有默认的无参构造函数,有析构函数,struct没有默认的无参构造函数,且只能声明有参的构造函数,没有析构函数;
(5)class可以使用abstract和sealed,有protected修饰符,struct不可以用abstract和sealed,没有protected修饰符;
(6)class必须使用new初始化,结构可以不用new初始化;
(7) class实例由垃圾回收机制来保证内存的回收处理,而struct变量使用完后立即自动解除内存分配;
从职能观点来看,class表现为行为,而struct常用于存储数据;
作为参数传递时,class变量以按址方式传递,而struct变量是以按值方式传递的。
如何选择使用结构还是类:
1).堆栈的空间有限,对于大量的逻辑的对象,创建类要比创建结构好一些
2).结构表示如点、矩形和颜色这样的轻量对象,例如,如果声明一个含有 1000 个点对象的数组,则将为引用每个对象分配附加的内存。在此情况下,结构的成本较低
3).在表现抽象和多级别的对象层次时,类是最好的选择
4).大多数情况下该类型只是一些数据时,结构是最佳的选择
11. 简述assert()用法与注意事项:
(1) 在函数开始处检验传入参数的合法性;
(2) 每个assert只检验一个条件,因为同时检验多个条件时,如果断言失败,无法直观的判断是哪个条件失败;
(3) 不能使用改变环境的语句,因为assert只在DEBUG个生效,如果这么做,会使用程序在真正运行时遇到问题;
(4) assert和后面的语句应空一行,以形成逻辑和视觉上的一致感;
(5) 有的地方,assert不能代替条件过滤。
12. windows消息系统由哪几部分构成?
由以下3部分组成:
1. 消息队列:操作系统负责为进程维护一个消息队列,程序运行时不断从该消息队列中获取消息、处理消息;
2. 消息循环:应用程序通过消息循环不断获取消息、处理消息。
3. 消息处理:消息循环负责将消息派发到相关的窗口上使用关联的窗口过程函数进行处理。
13. 在头文件中进行类的声明,在对应的实现文件中进行类的定义有什么意义?
一般都是代表一个基本功能的源文件引用相应的头文件。一个 相关功能的模块可能有若干对源文件和头文件组成。这是基于组件编程的核心。c语言中头文件中一般定义了函数的声明、结构体的定义、宏定义。(常量和全局变量最好放到源文件中).
优势:
1) 从业务扩展性上看:头文件中放函数的声明,函数由源文件实现,这就是将面向接口编程:接口和实现分开,这在面对业务变更频繁的需求中技术实现的好处是显而易见的: 只要定义出良好地、扩展性高的接口,实现是可以很方便的更换。
2) 从程序架构上看:代码在在大型程序中 需要分成不同的模块,单一模块中又可能分为不同的业务功能单元,他们间有很多相互的调用。头文件中的方法声明、结构体定义、宏就都可以充当这部分的模块与模块间、业务功能单位间的接口调用。模块与模块间,功能单元与功能单元间都是面向接口的调用,耦合性低,这正是基于组件编程的核心思想。(模块的高内聚低耦合)
3) 从某些技术角度实现上看:头文件可通过宏定义(头文件保护宏)来保证类定义、结构体定义、宏定义的唯一性。确实很方便,不容易出错.
(4) 在编译时,源文件里的实现会被编译成临时文件,运行时刻程序找到头文件里的接口,根据接口找到这些临时文件,来调用它们这些实现。这样可以提高编译效率,因为分开的话只需要编译一次生成对应的.obj文件后,再次应用该类的地方,这个类就不会被再次编译,从而大大的提高了编译效率。
14. main 函数执行之前/之后还会执行什么代码?
main 函数之前执行:
一些全局变量、对象和静态变量、对象的空间分配和赋初值就是在执行main函数之前;
main函数执行完后,还要去执行一些诸如释放空间、释放资源使用权等操作
全局对象的析构函数会在main函数之后执行;
用atexit注册的函数也会在main之后执行。
15. 将一个字符串逆序?
参见博文: http://www.cnblogs.com/graphics/archive/2011/03/09/1977717.html
16. 关于数组的几道面试题
参见: 关于数组的几道面试题
17. 请简单描述Windows内存管理的方法
内存管理是操作系统中的重要部分三言两语讲不清楚的.下面只做最简单的描述:
当程序运行时需要从内存中读出这段程序的代码。代码的位置必须在物理内存中才能被运行,由于现在的操作系统中有非常多的程序运行着,内存中不能够完全放下,所以引出了虚拟内存的概念。把哪些不常用的程序片断就放入虚拟内存,当需要用到它的时候在load入主存(物理内存)中。这个就是内存管理所要做的事。内存管理还有另外一件事需要做:计算程序片段在主存中的物理位置,以便CPU调度。
内存管理有块式管理,页式管理,段式和段页式管理。现在常用段页式管理.
(1) 块式管理:把主存分为一大块、一大块的,当所需的程序片断不在主存时就分配一块主存空间,把程 序片断load入主存,就算所需的程序片度只有几个字节也只能把这一块分配给它。这样会造成很大的浪费,平均浪费了50%的内存空间,但时易于管理。
(2) 页式管理:把主存分为一页一页的,每一页的空间要比一块一块的空间小很多,显然这种方法的空间利用率要比块式管理高很多。
(3) 段式管理:把主存分为一段一段的,每一段的空间又要比一页一页的空间小很多,这种方法在空间利用率上又比页式管理高很多,但是也有另外一个缺点。一个程序片断可能会被分为几十段,这样很多时间就会被浪费在计算每一段的物理地址上(计算机最耗时间的大家都知道是I/O吧)。
(4) 段页式管理:结合了段式管理和页式管理的优点。把主存分为若干页,每一页又分为若干段。
各种内存管理都有它自己的方法来计算出程序片断在主存中的物理地址,其实都很相似. 这只是一个大概而已,不足以说明内存管理的皮毛。无论哪一本操作系统书上都有详细的讲解.
18.求下面函数的返回值
- int func(x)
- {
- int countx = 0;
- while(x)
- {
- countx ++;
- x = x&(x-1);
- }
- return countx;
- }
假定x = 100 答案:countx = 8
思路:将x转化为2进制,看含有的1的个数。
19.重载(overload)和重写(overried,也称“覆盖”)的区别?
常考的题目。从定义上来说:
重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
重写:是指子类重新定义父类虚函数的方法。
从实现原理上来说:
重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!
重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。
20. C++是不是类型安全的?
不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。
21. 将程序跳转到指定内存地址要对绝对地址0x100000赋值,我们可以用(unsigned int*)0x100000 = 1234;那么要是想让程序跳转到绝对地址是0x100000去执行,应该怎么做?
- *((void (*)( ))0x100000 ) ( );
- //首先要将0x100000强制转换成函数指针,即:
- (void (*)())0x100000
- // 然后再调用它:
- *((void (*)())0x100000)();
- // 用typedef可以看得更直观些:
- typedef void(*)() voidFuncPtr;
- *((voidFuncPtr)0x100000)();
22. 复杂声明
- void * ( * (*fp1)(int))[10];
- float (*(* fp2)(int,int,int))(int);
- int (* ( * fp3)())[10]();
分别表示什么意思?
1.void * ( * (*fp1)(int))[10]; fp1是一个指针,指向一个函数,这个函数的参数为int型,函数的返回值也是一个指针,这个返回指针指向一个数组,这个数组有10个元素,每个元素是一个void*型指针。
2.float (*(* fp2)(int,int,int))(int); fp2是一个指针,指向一个函数,这个函数的参数为3个int型,函数的返回值也是一个指针,这个返回指针指向一个函数,这个函数的参数为int型,函数的返回值是float型。
3.int (* ( * fp3)())[10](); fp3是一个指针,指向一个函数,这个函数的参数为空,函数的返回值也是一个指针,这个返回指针指向一个数组,这个数组有10个元素,每个元素是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是int型。
C/C++经典面试题
面试题 1:变量的声明和定义有什么区别
为变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个地方声明,但是只在一个地方定义。加入 extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。
说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。
面试题 2:写出 bool 、int、 float、指针变量与“零值”比较的 if 语句
- //bool 型数据:
- if( flag )
- {
- A;
- }
- else
- {
- B;
- }
- // int 型数据:
- if( 0 != flag )
- {
- A;
- }
- else
- {
- B;
- }
- // 指针型数:
- if( NULL == flag )
- {
- A;
- }
- else
- {
- B;
- }
- //float 型数据:
- if ( ( flag >= NORM ) && ( flag <= NORM ) )
- {
- A;
- }
注意:应特别注意在 int、指针型变量和“零值”比较的时候,把“零值”放在左边,这样当把“==”误写成“=”时,编译器可以报错,否则这种逻辑错误不容易发现,并且可能导致很严重的后果。
面试题 3:sizeof 和 strlen 的区别
sizeof 和 strlen 有以下区别:
sizeof 是一个操作符,strlen 是库函数。
sizeof 的参数可以是数据的类型,也可以是变量,而 strlen 只能以结尾为‘\0‘的字符串作参数。
编译器在编译时就计算出了 sizeof 的结果。而 strlen 函数必须在运行时才能计算出来。并且 sizeof计算的是数据类型占内存的大小,而 strlen 计算的是字符串实际的长度。
数组做 sizeof 的参数不退化,传递给 strlen 就退化为指针了。
注意:有些是操作符看起来像是函数,而有些函数名看起来又像操作符,这类容易混淆的名称一定要加以区分,否则遇到数组名这类特殊数据类型作参数时就很容易出错。最容易混淆为函数的操作符就是 sizeof。
面试题 4:C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
在 C 中 static 用来修饰局部静态变量和外部静态变量、函数。而 C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。
注意:编程时 static 的记忆性,和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,而 C++的静态成员则可以在多个对象实例间进行通信,传递信息。
面试题 5:C中的 malloc 和C++中的 new 有什么区别
malloc 和 new 有以下不同:
(1)new、delete 是操作符,可以重载,只能在 C++中使用。
(2)malloc、free 是函数,可以覆盖,C、C++中都可以使用。
(3)new 可以调用对象的构造函数,对应的 delete 调用相应的析构函数。
(4)malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数
(5)new、delete 返回的是某种数据类型指针,malloc、free 返回的是 void 指针。
注意:malloc 申请的内存空间要用 free 释放,而 new 申请的内存空间要用 delete 释放,不要混用。因为两者实现的机理不同。
面试题 6:写一个“标准”宏 MIN
- #define min(a,b)((a)<=(b)?(a):(b))
注意:在调用时一定要注意这个宏定义的副作用,如下调用:
- ((++*p)<=(x)?(++*p):(x)
p 指针就自加了两次,违背了 MIN 的本意。
面试题 7:一个指针可以是 volatile 吗
可以,因为指针和普通变量一样,有时也有变化程序的不可控性。常见例:子中断服务子程序修改一个指向一个 buffer 的指针时,必须用 volatile 来修饰这个指针。
说明:指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。
面试题 8:a 和&a 有什么区别
请写出以下代码的打印结果,主要目的是考察 a 和&a 的区别。
- #include<stdio.h>
- void main( void )
- {
- int a[5]={1,2,3,4,5};
- int *ptr=(int *)(&a+1);
- printf("%d,%d",*(a+1),*(ptr-1));
- return;
- }
输出结果:2,5。
注意:数组名 a 可以作数组的首地址,而&a 是数组的指针。思考,将原式的 int *ptr=(int *)(&a+1);改为 int *ptr=(int *)(a+1);时输出结果将是什么呢?
面试题 9:简述 C、C++程序编译的内存分配情况
C、C++中内存分配方式可以分为三种:
(1)从静态存储区域分配:
内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错,因为有系统会善后。例如全局变量,static 变量等。
(2)在栈上分配:
在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3)从堆上分配:
即动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
一个 C、C++程序编译时内存分为 5 大存储区:堆区、栈区、全局区、文字常量区、程序代码区。
面试题 10:简述 strcpy、sprintf 与 memcpy 的区别
三者主要有以下不同之处:
(1)操作对象不同, strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串, memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
(2)执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
(3)实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。
说明:strcpy、sprintf 与 memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。
面试题 11:设置地址为 0x67a9 的整型变量的值为 0xaa66
- int *ptr;
- ptr = (int *)0x67a9;
- *ptr = 0xaa66;
说明:这道题就是强制类型转换的典型例子,无论在什么平台地址长度和整型数据的长度是一样的,即一个整型数据可以强制转换成地址指针类型,只要有意义即可。
面试题 12:面向对象的三大特征
面向对象的三大特征是封装性、继承性和多态性:
封装性:将客观事物抽象成类,每个类对自身的数据和方法实行 protection(private, protected,public)。
继承性:广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。
多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
说明:面向对象的三个特征是实现面向对象技术的关键,每一个特征的相关技术都非常的复杂,程序员应该多看、多练。
面试题 13:C++的空类有哪些成员函数
缺省构造函数。
缺省拷贝构造函数。
缺省析构函数。
缺省赋值运算符。
缺省取址运算符。
缺省取址运算符 const。
注意:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。
面试题 14:谈谈你对拷贝构造函数和赋值运算符的认识
拷贝构造函数和赋值运算符重载有以下两个不同之处:
(1)拷贝构造函数生成新的类对象,而赋值运算符不能。
(2)由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉.
注意:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。
面试题 15:用 C++设计一个不能被继承的类
- template <typename T> class A
- {
- friend T;
- private:
- A() {}
- ~A() {}
- };
- class B : virtual public A<B>
- {
- public:
- B() {}
- ~B() {}
- };
- class C : virtual public B
- {
- public:
- C() {}
- ~C() {}
- };
- void main( void )
- {
- B b;
- //C c;
- return;
- }
注意:构造函数是继承实现的关键,每次子类对象构造时,首先调用的是父类的构造函数,然后才是自己的。
面试题 16:访问基类的私有虚函数
写出以下程序的输出结果:
- #include <iostream.h>
- class A
- {
- virtual void g()
- {
- cout << "A::g" << endl;
- }
- private:
- virtual void f()
- {
- cout << "A::f" << endl;
- }
- };
- class B : public A
- {
- void g()
- {
- cout << "B::g" << endl;
- }
- virtual void h()
- {
- cout << "B::h" << endl;
- }
- };
- typedef void( *Fun )( void );
- void main()
- {
- B b;
- Fun pFun;
- for(int i = 0 ; i < 3; i++)
- {
- pFun = ( Fun )*( ( int* ) * ( int* )( &b ) + i );
- pFun();
- }
- }
输出结果:
B::g
A::f
B::h
注意:本题主要考察了面试者对虚函数的理解程度。一个对虚函数不了解的人很难正确的做出本题。在学习面向对象的多态性时一定要深刻理解虚函数表的工作原理。
面试题 17:简述类成员函数的重写、重载和隐藏的区别
(1)重写和重载主要有以下几点不同。
范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。
virtual 的区别:重写的基类中被重写的函数必须要有 virtual 修饰,而重载函数和被重载函数可以被virtual 修饰,也可以没有。
(2)隐藏和重写、重载有以下几点不同。
与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。
当参数不相同时,无论基类中的参数是否被 virtual 修饰,基类的函数都是被隐藏,而不是被重写。
说明:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。
面试题 18:简述多态实现的原理
编译器发现一个类中有虚函数,便会立即为此类生成虚函数表 vtable。虚函数表的各表项为指向对应虚函数的指针。编译器还会在此类中隐含插入一个指针 vptr(对 vc 编译器来说,它插在类的第一个位置上)指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行 vptr 与 vtable 的关联代码,将 vptr 指向对应的 vtable,将类与此类的 vtable 联系了起来。另外在调用类的构造函数时,
指向基础类的指针此时已经变成指向具体的类的 this 指针,这样依靠此 this 指针即可得到正确的 vtable。如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。
注意:一定要区分虚函数,纯虚函数、虚拟继承的关系和区别。牢记虚函数实现原理,因为多态C++面试的重要考点之一,而虚函数是实现多态的基础。
面试题 19:链表和数组有什么区别
数组和链表有以下几点不同:
(1)存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点指针。
(2)数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点,效率低。
(3)数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。
(4)越界问题:链表不存在越界问题,数组有越界问题。
说明:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。
面试题 20:怎样把一个单链表反序
- // (1)反转一个链表。循环算法。
- AList reverse(List n)
- {
- if(!n) //判断链表是否为空,为空即退出。
- {
- return n;
- }
- list cur = n.next; //保存头结点的下个结点
- list pre = n; //保存头结点
- list tmp;
- pre.next = null; //头结点的指针指空,转换后变尾结点
- while ( NULL != cur.next ) //循环直到 cur.next 为空
- {
- tmp = cur;
- tmp.next = pre
- pre = tmp;
- cur = cur.next;
- }
- return tmp; //f 返回头指针
- }
- // (2)反转一个链表。递归算法。
- List *reverse( List *oldList, List *newHead = NULL )
- {
- List *next = oldList-> next; //记录上次翻转后的链表
- oldList-> next = newHead; //将当前结点插入到翻转后链表的开头
- newHead = oldList; //递归处理剩余的链表
- return ( next==NULL )? newHead: reverse( t, newHead );
- }
说明:循环算法就是图 10.2—图 10.5 的移动过程,比较好理解和想到。递归算法的设计虽有一点难度,但是理解了循环算法,再设计递归算法就简单多了。
面试题 21:简述队列和栈的异同
队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是“后进先出”。
注意:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收。分配方式类似于链表。它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。
面试题 22:能否用两个栈实现一个队列的功能
- // 结点结构体:
- typedef struct node
- {
- int data;
- node *next;
- }node,*LinkStack;
- //创建空栈:
- LinkStack CreateNULLStack( LinkStack &S)
- {
- S = (LinkStack)malloc( sizeof( node ) ); //申请新结点
- if( NULL == S)
- {
- printf("Fail to malloc a new node.\n");
- return NULL;
- }
- S->data = 0; //初始化新结点
- S->next = NULL;
- return S;
- }
- // 栈的插入函数:
- LinkStack Push( LinkStack &S, int data)
- {
- if( NULL == S) //检验栈
- {
- printf("There no node in stack!");
- return NULL;
- }
- LinkStack p = NULL;
- p = (LinkStack)malloc( sizeof( node ) ); //申请新结点
- if( NULL == p)
- {
- printf("Fail to malloc a new node.\n");
- return S;
- }
- if( NULL == S->next)
- {
- p->next = NULL;
- }
- else
- {
- p->next = S->next;
- }
- p->data = data; //初始化新结点
- S->next = p; //插入新结点
- return S;
- }
- // 出栈函数:
- node Pop( LinkStack &S)
- {
- node temp;
- temp.data = 0;
- temp.next = NULL;
- if( NULL == S) //检验栈
- {
- printf("There no node in stack!");
- return temp;
- }
- temp = *S;
- if( S->next == NULL )
- {
- printf("The stack is NULL,can't pop!\n");
- return temp;
- }
- LinkStack p = S ->next; //节点出栈
- S->next = S->next->next;
- temp = *p;
- free( p );
- p = NULL;
- return temp;
- }
- // 双栈实现队列的入队函数:
- LinkStack StackToQueuPush( LinkStack &S, int data)
- {
- node n;
- LinkStack S1 = NULL;
- CreateNULLStack( S1 ); //创建空栈
- while( NULL != S->next ) //S 出栈入 S1
- {
- n = Pop( S );
- Push( S1, n.data );
- }
- Push( S1, data ); //新结点入栈
- while( NULL != S1->next ) //S1 出栈入 S
- {
- n = Pop( S1 );
- Push( S, n.data );
- }
- return S;
- }
说明:用两个栈能够实现一个队列的功能,那用两个队列能否实现一个队列的功能呢?结果是否定的,因为栈是先进后出,将两个栈连在一起,就是先进先出。而队列是现先进先出,无论多少个连在一起都是先进先出,而无法实现先进后出。
面试题 23:计算一颗二叉树的深度
- // 深度的计算函数:
- int depth(BiTree T)
- {
- if(!T) return 0;
- //判断当前结点是否为叶子结点
- int d1= depth(T->lchild);//求当前结点的左孩子树的深度
- int d2= depth(T->rchild);//求当前结点的右孩子树的深度
- return (d1>d2?d1:d2)+1;
- }
注意:根据二叉树的结构特点,很多算法都可以用递归算法来实现。
面试题 24:编码实现直接插入排序
- // 直接插入排序编程实现如下:
- #include<iostream.h>
- void main( void )
- {
- int ARRAY[10] = { 0, 6, 3, 2, 7, 5, 4, 9, 1, 8 };
- int i,j;
- for( i = 0; i < 10; i++)
- {
- cout<<ARRAY[i]<<" ";
- }
- cout<<endl;
- for( i = 2; i <= 10; i++ ) //将 ARRAY[2],...,ARRAY[n]依次按序插入
- {
- if(ARRAY[i] < ARRAY[i-1]) //如果 ARRAY[i]大于一切有序的数值,
- //ARRAY[i]将保持原位不动
- {
- ARRAY[0] = ARRAY[i]; //将 ARRAY[0]看做是哨兵,是 ARRAY[i]的副本
- j = i - 1;
- do{
- //从右向左在有序区 ARRAY[1..i-1]中
- //查找 ARRAY[i]的插入位置
- ARRAY[j+1] = ARRAY[j]; //将数值大于 ARRAY[i]记录后移
- j-- ;
- }while( ARRAY[0] < ARRAY[j] );
- ARRAY[j+1]=ARRAY[0]; //ARRAY[i]插入到正确的位置上
- }
- }
- for(i = 0; i < 10; i++)
- {
- cout<<ARRAY[i]<<" ";
- }
- cout<<endl;
- }
注意:所有为简化边界条件而引入的附加结点(元素)均可称为哨兵。引入哨兵后使得查找循环条件的时间大约减少了一半,对于记录数较大的文件节约的时间就相当可观。类似于排序这样使用频率非常高的算法,要尽可能地减少其运行时间。所以不能把上述算法中的哨兵视为雕虫小技。
面试题 25:编码实现冒泡排序
- // 冒泡排序编程实现如下:
- #include <stdio.h>
- #define LEN 10 //数组长度
- void main( void )
- {
- int ARRAY[10] = { 0, 6, 3, 2, 7, 5, 4, 9, 1, 8 }; //待排序数组
- printf( "\n" );
- for( int a = 0; a < LEN; a++ ) //打印数组内容
- {
- printf( "%d ", ARRAY[a] );
- }
- int i = 0;
- int j = 0;
- bool isChange; //设定交换标志
- for( i = 1; i < LEN; i++ )
- { //最多做 LEN-1 趟排序
- isChange = 0; //本趟排序开始前,交换标志应为假
- for( j = LEN-1; j >= i; j-- ) //对当前无序区 ARRAY[i..LEN]自下向上扫描
- {
- if( ARRAY[j+1] < ARRAY[j] )
- { //交换记录
- ARRAY[0] = ARRAY[j+1]; //ARRAY[0]不是哨兵,仅做暂存单元
- ARRAY[j+1] = ARRAY[j];
- ARRAY[j] = ARRAY[0];
- isChange = 1; //发生了交换,故将交换标志置为真
- }
- }
- printf( "\n" );
- for( a = 0; a < LEN; a++)
- //打印本次排序后数组内容
- {
- printf( "%d ", ARRAY[a] );
- }
- if( !isChange )
- {
- break; //本趟排序未发生交换,提前终止
- }
- }
- printf( "\n" );
- return;
- }
26:编码实现直接选择排序
- 26:编码实现直接选择排序
- #include"stdio.h"
- #define LEN 9
- void main( void )
- {
- int ARRAY[LEN]={ 5, 6, 8, 2, 4, 1, 9, 3, 7 }; //待序数组
- printf("Before sorted:\n");
- for( int m = 0; m < LEN; m++ ) //打印排序前数组
- {
- printf( "%d", ARRAY[m] );
- }
- for (int i = 1; i <= LEN - 1; i++) //选择排序
- {
- int t = i - 1;
- int temp = 0;
- for (int j = i; j < LEN; j++)
- {
- if (ARRAY[j] < ARRAY[t])
- {
- t = j;
- }
- }
- if (t != (i - 1))
- {
- temp = ARRAY[i - 1];
- ARRAY[i - 1] = ARRAY[t];
- ARRAY[t] = temp;
- }
- }
- printf( "\n" );
- printf("After sorted:\n");
- for( i = 0; i < LEN; i++ ) //打印排序后数组
- {
- printf( "%d ", ARRAY[i] );
- }
- printf( "\n" );
- }
注意:在直接选择排序中,具有相同关键码的对象可能会颠倒次序,因而直接选择排序算法是一种不稳定的排序方法。在本例中只是例举了简单的整形数组排序,肯定不会有什么问题。但是在复杂的数据元素序列组合中,只是根据单一的某一个关键值排序,直接选择排序则不保证其稳定性,这是直接选择排序的一个弱点。
面试题 27:编程实现堆排序
- // 堆排序编程实现:
- #include <stdio.h>
- void createHeep(int ARRAY[],int sPoint, int Len) //生成大根堆
- {
- while( ( 2 * sPoint + 1 ) < Len )
- {
- int mPoint = 2 * sPoint + 1 ;
- if( ( 2 * sPoint + 2 ) < Len )
- {
- if(ARRAY[ 2 * sPoint + 1 ] < ARRAY[ 2 * sPoint + 2 ] )
- {
- mPoint = 2*sPoint+2;
- }
- }
- if(ARRAY[ sPoint ] < ARRAY[ mPoint ]) //堆被破坏,需要重新调整
- {
- int tmpData= ARRAY[ sPoint ]; //交换 sPoint 与 mPoint 的数据
- ARRAY[ sPoint ] = ARRAY[ mPoint ];
- ARRAY[ mPoint ] = tmpData;
- sPoint = mPoint ;
- }
- else
- {
- break; //堆未破坏,不再需要调整
- }
- }
- return;
- }
- void heepSort( int ARRAY[], int Len ) //堆排序
- {
- int i=0;
- for ( i = ( Len / 2 - 1 ); i >= 0; i-- ) //将 Hr[0,Lenght-1]建成大根堆
- {
- createHeep(ARRAY, i, Len);
- }
- for ( i = Len - 1; i > 0; i-- )
- {
- int tmpData = ARRAY[0]; //与最后一个记录交换
- ARRAY[0] = ARRAY[i];
- ARRAY[i] = tmpData;
- createHeep( ARRAY, 0, i ); //将 H.r[0..i]重新调整为大根堆
- }
- return;
- }
- int main( void )
- {
- int ARRAY[] ={ 5, 4, 7, 3, 9, 1, 6, 8, 2};
- printf("Before sorted:\n"); //打印排序前数组内容
- for ( int i = 0; i < 9; i++ )
- {
- printf("%d ", ARRAY[i]);
- }
- printf("\n");
- heepSort( ARRAY, 9 ); //堆排序
- printf("After sorted:\n"); //打印排序后数组内容
- for( i = 0; i < 9; i++ )
- {
- printf( "%d ", ARRAY[i] );
- }
- printf( "\n" );
- return 0;
- }
说明:堆排序,虽然实现复杂,但是非常的实用。另外读者可是自己设计实现小堆排序的算法。虽然和大堆排序的实现过程相似,但是却可以加深对堆排序的记忆和理解。
面试题 28:编程实现基数排序
- #include <stdio.h>
- #include <malloc.h>
- #define LEN 8
- typedef struct node //队列结点
- {
- int data;
- struct node * next;
- }node,*QueueNode;
- typedef struct Queue //队列
- {
- QueueNode front;
- QueueNode rear;
- }Queue,*QueueLink;
- QueueLink CreateNullQueue( QueueLink &Q) //创建空队列
- {
- Q = NULL;
- Q = ( QueueLink )malloc( sizeof( Queue ) );
- if( NULL == Q )
- {
- printf("Fail to malloc null queue!\n");
- return NULL;
- }
- 15Q->front = ( QueueNode )malloc( sizeof( node ) );
- Q->rear = ( QueueNode )malloc( sizeof( node ) );
- if( NULL == Q->front || NULL == Q->rear )
- {
- printf("Fail to malloc a new queue's fornt or rear!\n");
- return NULL;
- }
- Q->rear = NULL;
- Q->front->next= Q->rear;
- return Q;
- }
- int lenData( node data[], int len) //计算队列中各结点的数据的最大位数
- {
- int m = 0;
- int temp = 0;
- int d;
- for( int i = 0; i < len; i++)
- {
- d = data[i].data;
- while( d > 0)
- {
- d /= 10;
- temp ++;
- }
- if( temp > m )
- {
- m = temp;
- }
- temp = 0;
- }
- return m;
- }
- QueueLink Push( QueueLink &Q , node node ) //将数据压入队列
- {
- QueueNode p1,p;
- p =( QueueNode )malloc( sizeof( node ) );
- if( NULL == p )
- {
- printf("Fail to malloc a new node!\n");
- return NULL;
- }
- p1 = Q->front;
- while(p1->next != NULL)
- {
- p1 = p1->next;
- }
- p->data = node.data;
- p1->next = p;
- p->next = Q->rear;
- return NULL;
- }
- node Pop( QueueLink &Q) //数据出队列
- {
- node temp;
- temp.data = 0;
- temp.next = NULL;
- QueueNode p;
- p = Q->front->next;
- if( p != Q->rear )
- {
- temp = *p;
- Q->front->next = p->next;
- free( p );
- p = NULL;
- }
- return temp;
- }
- int IsEmpty( QueueLink Q)
- {
- if( Q->front->next == Q->rear )
- {
- return 0;
- }
- return 1;
- }
- int main( void )
- {
- int i = 0;
- int Max = 0; //记录结点中数据的最大位数
- int d = 10;
- int power = 1;
- int k = 0;
- node Array[LEN] ={{450, NULL}, {32,NULL}, { 781,NULL}, { 57 ,NULL},{ 145,NULL},{ 613,NULL},{ 401,NULL},{ 594,NULL}}; //队列结点数组
- QueueLink Queue[10];
- for( i = 0; i < 10; i++)
- {
- CreateNullQueue( Queue[i]); //初始化队列数组
- }
- for( i = 0; i < LEN; i++)
- {
- printf("%d ",Array[i].data);
- }
- printf("\n");
- Max = lenData( Array, LEN ); //计算数组中关键字的最大位数
- printf("%d\n",Max);
- for(int j = 0; j < Max; j++) //按位排序
- {
- if(j == 0) power = 1;
- else power = power *d;
- for(i = 0; i < LEN; i++)
- {
- k = Array[i].data /power - (Array[i].data/(power * d)) * d;
- Push( Queue[k], Array[i] );
- }
- for(int l = 0, k = 0; l < d; l++) //排序后出队列重入数组
- {
- while( IsEmpty( Queue[l] ) )
- {
- Array[k++] = Pop( Queue[l] );
- }
- }
- for( int t = 0; t < LEN; t++)
- {
- printf("%d ",Array[t].data);
- }
- printf("\n");
- }
- return 0;
- }
说明:队列为基数排序的实现提供了很大的方便,适当的数据机构可以减少算法的复杂度,让更多的算法实现更容易。
面试题 29:谈谈你对编程规范的理解或认识
编程规范可总结为:程序的可行性,可读性、可移植性以及可测试性。
说明:这是编程规范的总纲目,面试者不一定要去背诵上面给出的那几个例子,应该去理解这几个例子说明的问题,想一想,自己如何解决可行性、可读性、可移植性以及可测试性这几个问题,结合以上几个例子和自己平时的编程习惯来回答这个问题。
面试题 30:short i = 0; i = i + 1L;这两句有错吗
代码一是错的,代码二是正确的。
说明:在数据安全的情况下大类型的数据向小类型的数据转换一定要显示的强制类型转换。
面试题 31:&&和&、||和|有什么区别
(1)&和|对操作数进行求值运算,&&和||只是判断逻辑关系。
(2)&&和||在在判断左侧操作数就能确定结果的情况下就不再对右侧操作数求值。
注意:在编程的时候有些时候将&&或||替换成&或|没有出错,但是其逻辑是错误的,可能会导致不可预想的后果(比如当两个操作数一个是 1 另一个是 2 时)。
面试题 32:C++的引用和 C 语言的指针有什么区别
指针和引用主要有以下区别:
(1)引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。
(2)引用初始化以后不能被改变,指针可以改变所指的对象。
(3)不存在指向空值的引用,但是存在指向空值的指针。
注意:引用作为函数参数时,会引发一定的问题,因为让引用作参数,目的就是想改变这个引用所指向地址的内容,而函数调用时传入的是实参,看不出函数的参数是正常变量,还是引用,因此可能会引发错误。所以使用时一定要小心谨慎。
面试题 33:在二元树中找出和为某一值的所有路径
输入一个整数和一棵二元树。从树的根结点开始往下访问,一直到叶结点所经过的所有结点形成一条路径。打印出和与输入整数相等的所有路径。例如,输入整数 9 和如下二元树:
3
/ \
2 6
/ \
5 4
则打印出两条路径:3,6 和 3,2,4。
【答案】
- typedef struct path
- {
- BiTNode* tree;//结点数据成员
- struct path* next;//结点指针成员
- }PATH,*pPath;
- // 初始化树的结点栈:
- void init_path( pPath* L )
- {
- *L = ( pPath )malloc( sizeof( PATH ) ); //创建空树
- ( *L )->next = NULL;
- }
- // 树结点入栈函数:
- void push_path(pPath H, pBTree T)
- {
- pPath p = H->next;
- pPath q = H;
- while( NULL != p )
- {
- q = p;
- p = p->next;
- }
- p = ( pPath )malloc( sizeof( PATH ) ); //申请新结点
- p->next = NULL; //初始化新结点
- p->tree = T;
- q->next = p; //新结点入栈
- }
- // 树结点打印函数:
- void print_path( pPath L )
- {
- pPath p = L->next;
- while( NULL != p ) //打印当前栈中所有数据
- {
- printf("%d, ", p->tree->data);
- p = p->next;
- }
- }
- // 树结点出栈函数:
- void pop_path( pPath H )
- {
- pPath p = H->next;
- pPath q = H;
- if( NULL == p ) //检验当前栈是否为空
- {
- printf("Stack is null!\n");
- return;
- }
- p = p->next;
- while( NULL != p ) //出栈
- {
- q = q->next;
- p = p->next;
- }
- free( q->next ); //释放出栈结点空间
- q->next = NULL;
- }
- // 判断结点是否为叶子结点:
- int IsLeaf(pBTree T)
- {
- return ( T->lchild == NULL )&&( T->rchild==NULL );
- }
- // 查找符合条件的路径:
- int find_path(pBTree T, int sum, pPath L)
- {
- push_path( L, T);
- record += T->data;
- if( ( record == sum ) && ( IsLeaf( T ) ) ) //打印符合条件的当前路径
- {
- print_path( L );
- printf( "\n" );
- }
- if( T->lchild != NULL ) //递归查找当前节点的左孩子
- {
- find_path( T->lchild, sum, L);
- }
- if( T->rchild != NULL ) //递归查找当前节点的右孩子
- {
- find_path( T->rchild, sum, L);
- }
- record -= T->data;
- pop_path(L);
- return 0;
- }
注意:数据结构一定要活学活用,例如本题,把所有的结点都压入栈,而不符合条件的结点弹出栈,很容易实现了有效路径的查找。虽然用链表也可以实现,但是用栈更利于理解这个问题,即适当的数据结构为更好的算法设计提供了有利的条件。
面试题 35:typedef 和 define 有什么区别
(1)用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义常量,以及书写复杂使用频繁的宏。
(2)执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
(3)作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在 define 声明后的引用都是正确的。
(4)对指针的操作不同:typedef 和 define 定义的指针时有很大的区别。
注意:typedef 定义是语句,因为句尾要加上分号。而 define 不是语句,千万不能在句尾加分号。
面试题 36:关键字 const 是什么?
const 用来定义一个只读的变量或对象。主要优点:便于类型检查、同宏定义一样可以方便地进行参数的修改和调整、节省空间,避免不必要的内存分配、可为函数重载提供参考。
说明:const 修饰函数参数,是一种编程规范的要求,便于阅读,一看即知这个参数不能被改变,实现时不易出错。
面试题 37:static 有什么作用
static 在 C 中主要用于定义全局静态变量、定义局部静态变量、定义静态函数。在 C++中新增了两种作用:定义静态数据成员、静态函数成员。
注意:因为 static 定义的变量分配在静态区,所以其定义的变量的默认值为 0,普通变量的默认值为随机数,在定义指针变量时要特别注意。
面试题 38:extern 有什么作用
extern 标识的变量或者函数声明其定义在别的文件中,提示编译器遇到此变量和函数时在其它模块中寻找其定义。
面试题 39:流操作符重载为什么返回引用
在程序中,流操作符>>和<<经常连续使用。因此这两个操作符的返回值应该是一个仍旧支持这两个操作符的流引用。其他的数据类型都无法做到这一点。
注意:除了在赋值操作符和流操作符之外的其他的一些操作符中,如+、-、*、/等却千万不能返回引用。因为这四个操作符的对象都是右值,因此,它们必须构造一个对象作为返回值。
面试题 40:简述指针常量与常量指针区别
指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。
面试题 41:数组名和指针的区别
请写出以下代码的打印结果:
- #include <iostream.h>
- #include <string.h>
- void main(void)
- {
- char str[13]="Hello world!";
- char *pStr="Hello world!";
- cout<<sizeof(str)<<endl;
- cout<<sizeof(pStr)<<endl;
- cout<<strlen(str)<<endl;
- cout<<strlen(pStr)<<endl;
- return;
- }
【答案】
打印结果:
13
4
12
12
注意:一定要记得数组名并不是真正意义上的指针,它的内涵要比指针丰富的多。但是当数组名当做参数传递给函数后,其失去原来的含义,变作普通的指针。另外要注意 sizeof 不是函数,只是操作符。
面试题 42:如何避免“野指针”
“野指针”产生原因及解决办法如下:
(1)指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向 NULL。
(2)指针 p 被 free 或者 delete 之后,没有置为 NULL。解决办法:指针指向的内存空间被释放后指针应该指向 NULL。
(3)指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向 NULL。
注意:
“野指针”的解决方法也是编程规范的基本原则,平时使用指针时一定要避免产生“野指针”,
在使用指针前一定要检验指针的合法性。
面试题 43:常引用有什么作用
常引用的引入主要是为了避免使用变量的引用时,在不知情的情况下改变变量的值。常引用主要用于定义一个普通变量的只读属性的别名、作为函数的传入形参,避免实参在调用函数中被意外的改变。
说明:很多情况下,需要用常引用做形参,被引用对象等效于常对象,不能在函数中改变实参的值,这样的好处是有较高的易读性和较小的出错率。
面试题 44:编码实现字符串转化为数字
编码实现函数 atoi(), 设计一个程序,把一个字符串转化为一个整型数值。例如数字:“5486321”,
转化成字符:5486321。
【答案】
- int myAtoi(const char * str)
- {
- int num = 0;//保存转换后的数值
- int isNegative = 0; //记录字符串中是否有负号
- int n =0;
- char *p = str;
- if(p == NULL) //判断指针的合法性
- {
- return -1;
- }
- while(*p++ != '\0') //计算数字符串度
- {
- n++;
- }
- p = str;
- if(p[0] == '-') //判断数组是否有负号
- {
- isNegative = 1;
- }
- char temp = '0';
- for(int i = 0 ; i < n; i++)
- {
- char temp = *p++;
- if(temp > '9' ||temp < '0') //滤除非数字字符
- {
- continue;
- }
- if(num !=0 || temp != '0') //滤除字符串开始的 0 字符
- {
- temp -= 0x30; //将数字字符转换为数值
- num += temp *int( pow(10 , n - 1 -i) );
- }
- }
- if(isNegative) //如果字符串中有负号,将数值取反
- {
- return (0 - num);
- }
- else
- {
- return num; //返回转换后的数值
- }
- }
注意:此段代码只是实现了十进制字符串到数字的转化,读者可以自己去实现 2 进制,8 进制,10进制,16 进制的转化。
面试题 45:简述 strcpy、sprintf 与 memcpy 的区别
三者主要有以下不同之处:
(1)操作对象不同, strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串, memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
(2)执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
(3)实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。
说明:strcpy、sprintf 与 memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来
选择合适的函数实现拷贝功能。
面试题 46:用 C 编写一个死循环程序
- while(1)
- { }
说明:很多种途径都可实现同一种功能,但是不同的方法时间和空间占用度不同,特别是对于嵌入式软件,处理器速度比较慢,存储空间较小,所以时间和空间优势是选择各种方法的首要考虑条件。
面试题 47:编码实现某一变量某位清 0 或置 1
给定一个整型变量 a,写两段代码,第一个设置 a 的 bit 3,第二个清 a 的 bit 3,在以上两个操作中,要保持其他位不变。
【答案】
- #define BIT3 (0x1 << 3 )
- Satic int a;
- //设置 a 的 bit 3:
- void set_bit3( void )
- {
- a |= BIT3;//将 a 第 3 位置 1
- }
- //清 a 的 bit 3
- void set_bit3( void )
- {
- a &= ~BIT3; //将 a 第 3 位清零
- }
说明:在置或清变量或寄存器的某一位时,一定要注意不要影响其他位。所以用加减法是很难实现的。
面试题 48:评论下面这个中断函数
中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展——让标准 C 支持中断。具体代表事实是,产生了一个新的关键字__interrupt。下面的代码就使用了__interrupt 关键字去定义一个中断服务子程序(ISR),请评论以下这段代码。
- __interrupt double compute_area (double radius)
- {
- double area = PI * radius * radius;
- printf(" Area = %f", area);
- return area;
- }
【答案】
这段中断服务程序主要有以下四个问题:
(1)ISR 不能返回一个值。
(2)ISR 不能传递参数。
(3)在 ISR 中做浮点运算是不明智的。
(4)printf()经常有重入和性能上的问题。
注意:本题的第三个和第四个问题虽不是考察的重点,但是如果能提到这两点可给面试官留下一个好印象。
面试题 49:构造函数能否为虚函数
构造函数不能是虚函数。而且不能在构造函数中调用虚函数,因为那样实际执行的是父类的对应函数,因为自己还没有构造好。析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。析构函数也可以是纯虚函数,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。说明:虚函数的动态绑定特性是实现重载的关键技术,动态绑定根据实际的调用情况查询相应类的虚函数表,调用相应的虚函数。
面试题 50:谈谈你对面向对象的认识
面向对象可以理解成对待每一个问题,都是首先要确定这个问题由几个部分组成,而每一个部分其实就是一个对象。然后再分别设计这些对象,最后得到整个程序。传统的程序设计多是基于功能的思想来进行考虑和设计的,而面向对象的程序设计则是基于对象的角度来考虑问题。这样做能够使得程序更加的简洁清晰。
说明:编程中接触最多的“面向对象编程技术”仅仅是面向对象技术中的一个组成部分。发挥面向对象技术的优势是一个综合的技术问题,不仅需要面向对象的分析,设计和编程技术,而且需要借助必要的建模和开发工具。
来源:oschina
链接:https://my.oschina.net/u/4305496/blog/3989040