部分内容复习
指向指针的指针的作用:当要修改实参指针的指向 的时候,形参 必须使用指向指针的指针。
当修改的是实参指针指向的内容 时,则形参 只需是指针即可
1 2 3 4 5 6 7 8 9 10 11 void GetMemory (char *p,int num) { p=(char *)malloc (sizeof (char )*num); } void main () { char *str=NULL ; GetMemory(str,100 ); strcpy (str,"hello" ); }
结构是编译能通过,却不能运行,为什么呢?
先说一下指针作为函数参数的意义:当将指针作为参数时,实参向形参传递的是地址,在函数执行过程中,既可以对该参数指针进行处理,也可以对该参数指针所指向的数据进行处理,(以上程序段来说就是可以对p或p进行处理)。*由于此时形参和实参都是指向同一个存储单元,因此当形参指针所指向的数据改变时,实参指针所指向的数据也作相应的改变,因此这时的形参可以作为输出参数使用。 (str和p应同时更改!)
按照上面的说法,这个程序应该没有问题的啊,实参str和形参p指向同一个存储单元,给形参分配的内存单元应该也给实参分配了才对啊,问题就是在这里
实参和形参是指向同一个地址,它们只是指向相同,但它们自身的地址不是同时申请的,就是说p在申请内存时,相当于是把p的指向给改了,但是str的指向仍然没有改!所以尽管str调用了GetMemory,但它仍然是个空指针,所以进行strcpy是就不能运行
要使程序可以运行,只要小小的改动就行了(用指向指针的指针):1 2 3 4 5 6 7 8 9 10 11 void GetMemory (char **p,int num) { *p=(char *)malloc (sizeof (char )*num); } void main () { char *str=NULL ; GetMemory(&str,100 ); strcpy (str,"hello" ); free (str); }
C++对C语言的加强 namespace命名空间 C++命名空间基本常识 所谓namespace,是指标识符的各种可见范围 。C++标准程序库中的所有标识符都被定义于一个名为std的namespace中。
c++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h
当使用时,相当于在c中调用库函数,使用的是全局命名空间,也就是早期的c++实现;
当使用的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用cout。
在程序开头使用using namespace std;,即可使命名空间std内定义的所有标识符都有效
与C的区别
C中的命名空间
在C语言中只有一个全局作用域
C语言中所有的全局标识符共享同一个作用域
标识符之间可能发生冲突
C++中的命名空间
命名空间将全局作用域分成不同的部分
不同命名空间中的标识符可以同名而不会发生冲突
命名空间可以相互嵌套
全局作用域也叫默认命名空间
在程序中使用命名空间
定义命名空间1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 namespace ns1{ const int RATE=0.08 ; doublepay; doubletax () { return a*RATE; } namespacens2 { int age; } }
使用命名空间别名
1 2 namespace Television { ... }
可以用一个较短而易记的别名代替它。如: namespace TV=Television;//别名TV与原名Television等价
使用using命名空间成员名using nsl::Student 在用上面的using声明后,在其后程序中出现的Student就是隐含地指nsl::Student
注意事项 在程序中如果直接使用using namespace std可以减少在变量前添加std::的繁琐编程,但是另一方面在多文件编译时可能会导致不同文件中定义的任何标识符可能与std命名空间中的同名标识符冲突,因此可以考虑使用,若干个”using命名空间成员”声明来代替“using namespace命名空间”声明 ,如:1 2 3 4 5 using std::string;using std::cout;using std::cin;
等。
三目运算符
C语言返回变量的值 C++语言是返回变量本身C语言中的三目运算符返回的是变量值,不能作为左值使用C++中的三目运算符可直接返回变量本身,因此可以出现在程序的任何地方
1 2 3 4 5 6 7 8 9 10 int main (void ) { int a = 10 ; int b = 20 ; (a < b ? a : b ) = 30 ; printf ("a = %d, b = %d\n" , a, b); return 0 ; }
三目运算符可能返回的值中如果有一个是常量值,则不能作为左值使用(a < b ? 1 : b )= 30;
const增强 外链C++中const关键字的使用方法总结
C++中的const修饰的,是一个真正的常量,而不是C中变量(只读 )。在const修饰的常量编译期间,就已经确定下来了1 2 3 4 const int a = 10 ;int *p = (int *)&a;*p = 11 ;
C++中的const常量类似于宏定义const int c =5; 约等于 #define 5
C++中的const常量与宏定义不同 const常量是由编译器处理的,提供类型检查和作用域检查。 宏定义由预处理器处理,单纯的文本替换
枚举 c语言中枚举本质就是整型,枚举变量可以用任意整型赋值。 而c++中枚举变量,只能用被枚举出来的元素初始化,简单来说,就是用 名字 来代表 整数值
1 2 3 4 5 6 7 8 9 10 11 enum season {SPR=1 ,SUM,AUT,WIN};int main () { enum season s = SPR; s = SUM; cout << "s = " << s <<endl; return 0 ; }
引用
变量名,本身是一段内存的引用,即别名(alias)。引用可以看作一个已定义变量 的别名1 2 3 4 5 6 7 8 9 10 int a = 10 ; int &b = a; a = 11 ; { int *p = &a; *p = 12 ; cout << a <<endl; } b = 14 ; cout << "a = " <<a << ", b = " << b <<endl;
引用没有定义,是一种关系型声明。声明它和原有某一变量(实体)的关系。故而类型与原类型保持一致,且不分配内存。与被引用的变量有相同的地址
&符号前有数据类型时,是引用。其它皆为取地址
声明的时候必须初始化,一经声明,不可变更
可对引用,再次引用。多次引用的结果,是某一变量具有多个别名
1 2 3 4 5 int a,b;int &r = a;int &r = b; float &rr = b; int &ra = r;
引用的意义
引用作为其它变量的别名而存在,因此在一些场合可以代替指针
引用相对于指针来说具有更好的可读性和实用性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void swap (int a, int b) ; void swap (int *p, int *q) ; void swap (int &a, int &b) { int tmp; tmp = a; a = b; b = tmp; } int main () { int a = 3 ,b = 5 ; cout<<"a = " <<a<<"b = " <<b<<endl; swap (a,b); cout<<"a = " <<a<<"b = " <<b<<endl; return 0 ; }
引用的本质 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int main () { int a = 10 ; int &b = a; b = 11 ; printf ("a:%d\n" , a); printf ("b:%d\n" , b); printf ("&a:%p\n" , &a); printf ("&b:%p\n" , &b); return 0 ; } a:11 b:11 &a:000000000061 fe14 &b:000000000061 fe14
可见得Cpp编译器在定义引用时是将两个变量名指向同一个内存地址:引用在C++中的内部实现是一个常指针Type& name <===> Type* const name
C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同
从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是C++为了实用性而做出的细节隐藏。
间接赋值的三个必要条件
定义两个变量 (一个实参一个形参)
建立关联 实参取地址传给形参
*p形参去间接的修改实参的值
引用在实现上,只不过是把:间接赋值成立的三个条件的后两步和二为一 当实参传给形参引用的时候,只不过是c++编译器帮我们程序员手工取了一个实参地址,传给了形参引用(常量指针)。
引用作为函数的返回值(引用当左值)
当函数返回值为引用时,若返回栈变量:不能成为其它引用的初始值(不能作为右值使用)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int getA1 () { int a; a = 10 ; return a; } int & getA2 () { int a; a = 10 ; return a; } int &a3 = getA2 ();
当函数返回值为引用时,若返回静态变量或全局变量可以成为其他引用的初始值(可作为右值使用,也可作为左值使用)
1 2 3 4 5 6 7 8 9 int & getA2 () { static int a; a = 10 ; return a; } int &a3 = getA2 ();
引用作为函数返回值,如果返回值为引用可以当左值,如果返回值为普通变量不可以当左值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int func1 () { static int a1 = 10 ; return a1; } int & func2 () { static int a2 = 10 ; return a2; } int main (void ) { int c1 = func1 (); cout << "c1 = " << c1 <<endl; int c2 = func2 (); cout << "c2 = " << c2 <<endl; func2 () = 100 ; c2 = func2 (); cout << "c2 = " << c2 <<endl; return 0 ; }
指针引用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 struct Teacher { char name[64 ]; int age ; }; int getTeacher (Teacher **p) { Teacher *tmp = NULL ; tmp = (Teacher*)malloc (sizeof (Teacher)); tmp->age = 33 ; *p = tmp; return 0 ; } int getTeacher2 (Teacher* &myp) { myp = (Teacher *)malloc (sizeof (Teacher)); if (myp == NULL ) return -1 ; myp->age = 36 ; return 0 ; } void FreeTeacher (Teacher *pT1) { if (pT1 == NULL ) return ; free (pT1); } int main (void ) { Teacher *pT1 = NULL ; getTeacher (&pT1); cout << "age:" << pT1 -> age << endl; FreeTeacher (pT1); getTeacher2 (pT1); cout << "age:" << pT1 -> age << endl; FreeTeacher (pT1); return 0 ; }
const 引用 const引用,它可以防止对象的值被随意修改。具有一些特性。
const对象的引用必须是const的,将普通引用绑定到const对象是不合法的
1 2 const int a = 1 ;int &b = a;
const引用可使用相关类型的对象(常量,非同类型的变量或表达式)初始化 。这个是const引用与普通引用最大的区别。
1 2 3 4 const int &a = 2 ;double x =3.1 ;const int &b =a;
1 2 3 4 5 6 7 8 int a = 10 ;int &b = a;int x = 20 ;const int &y = x;
const 引用的原理 const引用是指向const对象的引用: 通过ref2对ival赋值会导致修改const对象的值,为防止这样的修改,需要规定将普通的引用绑定到const对象是不合法的。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { const int ival =1024 ; const int &refVal = ival; int &ref2 = ival; } { double val =3.14 ; const int & ref = val; double & ref2 = val; cout << ref << " " << ref2 <<endl; val = 5.22 ; cout << ref << " " << ref2 <<endl; }
同样的初始化对于非const引用却是不合法的,而且会导致编译时错误,观察将引用绑定到不同的类型时所发生的事情,最容易理解上述行为。对于以下代码1 2 3 4 5 6 double dval = 3.14 ;const int &ri = dval;int temp = dval;const int &ri = temp;
可以发现对于将引用绑定到不同类型时,编译器会创建一个int型的暂时变量存储dval,然后将ri绑定到temp上
(Tips:引用在内部存放的是一个对象的地址,它是该对象的别名。对于不可寻址的值,如文字常量,以及不同类型的对象,编译器为了实现引用,必须生成一个临时对象,引用实际上指向该对象,但用户不能访问它。)
结论
const int & e 相当于 const int * const e
普通引用 相当于 int *const e
当使用常量(字面量)对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名
使用字面量对const引用初始化后,将生成一个只读变量
inline内联函数 类似于C语言中宏函数,但是C中的宏函数处理发生在预处理阶段,没有语法检测。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 inline void func (int a) { a = 20 ; cout << a <<endl; } int main (void ) { func (10 ); return 0 ; }
特点:
内联函数声明时inline关键字必须和函数定义结合在一起,否则编译器会直接忽略内联请求。
C++编译器直接将函数体插入在函数调用的地方。
内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)。
内联函数是一种特殊的函数,具有普通函数的特征(参数检查,返回类型等)。
内联函数由编译器处理,直接将编译后的函数体插入调用的地方,宏代码片段由预处理器处理,进行简单的文本替换,没有任何编译过程。
C++的内联编译应该不包含循环、过多的条件判断、不宜实现太过复杂 的的功能。
内联函数相对于普通函数的优势只是省去了函数调用时压栈,跳转和返回的开销 。因此,当函数体的执行开销远大于压栈,跳转和返回所用的开销时,那么内联将无意义。
总结:
本质:以牺牲代码段空间为代价,提高程序的运行时间的效率。
适用场景:函数体很“小 ”,且被“频繁 ”调用
函数默认参数 单个默认参数 对于多次调用一函数同一实参时,C++给出了更简单的处理办法。给形参以默认值,这样就不用从实参那里取值了。
1 2 3 4 5 void myPrint (int x = 3 ) { cout<<"x: " <<x<< endl; }
多个默认参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 float volume (float length, float weight = 4 ,float high = 5 ) { return length*weight*high; } int main () { float v = volume (10 ); float v1 = volume (10 ,20 ); float v2 = volume (10 ,20 ,30 ); cout<<v<<endl; cout<<v1<<endl; cout<<v2<<endl; return 0 ; }
占位参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 int func (int a, int b, int ) { return a + b; } int main () { func (1 ,2 ); func (1 ,2 ,3 ); } int func2 (int a, int b, int = 0 ) { return a + b; } int main () { func2 (1 , 2 ); func2 (1 , 2 , 3 ); return 0 ; }
函数重载 规则(类似Java)
函数名相同。
参数个数不同,参数的类型不同,参数顺序不同,均可构成重载。
返回值类型不同则不可以构成重载。
一个函数,不能既作重载,又作默认参数的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int func (int a, int b, int c = 0 ) { return a * b * c; } int func (int a, int b) { return a + b; } int func (int a) { return a; } int main () { int c = 0 ; c = func (1 , 2 ); printf ("c = %d\n" , c); return 0 ; }
C++基本 头文件保护 引出:重复定义问题 考虑以下示例:File_1.h 头文件包含以下内容1 2 3 4 int getNumber () { return 4 ; }
File_2.h
main.cpp 1 2 3 4 5 6 7 #include "File_1.h" #include "File_2.h" int main () { return 0 ; }
这个看似正常的程序无法编译 !下面是所发生的事情。首先,main.cpp包含File_1.h,它将函数getNumber的定义复制到main.cpp中。然后,main.cpp包含File_2.h,间接包含File_1.h。这会将File_1.h的内容(包括函数getNumber的定义)复制到File_2.h中,然后将其复制到main.cpp中。
因此,在解析所有#include之后,main.cpp最终如下所示:1 2 3 4 5 6 7 8 9 10 11 12 13 int getNumber () { return 4 ; } int getNumber () { return 4 ; } int main () { return 0 ; }
或者是下面这个例子 假设你有两个类:1 2 3 #include "B.h" class A { B b; };
1 2 3 #include "A.h" class B { A a; };
互相 include。
如果头文件里面 没有 include guard 编译器在展开 #include 的时候,会无限互相展开 → 死循环。
或者即使没有互相 include 一个头文件被 include 多次,类会重复定义:error: redefinition of class X
头文件保护 我们可以通过一种称为头文件保护(include guard 也称为包含保护)的机制来避免上述问题。头文件保护是采用以下形式的条件编译指令:1 2 3 4 5 6 7 8 #ifndef SOME_UNIQUE_NAME_HERE #define SOME_UNIQUE_NAME_HERE #endif
基本调试策略
注释代码
验证代码流程 在需要验证的代码前添加调试输出语句。 打印用于调试的信息时,请使用std::cerr而不是std:∶cout。因为,std::cout可能有缓冲区而std::cerr没有缓冲,这意味着您发送给它的任何内容都将立即输出
条件化调试代码 在引入调试代码后,完成调试后,您要么需要删除它们,要么将它们注释掉。如果以后需要它们,则必须重新添加它们,或取消注释它们。非常麻烦且容易扰乱正常的代码功能。 使用预处理指令,可以很容易的在整个程序中启用或禁用调试语句1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <iostream> #define ENABLE_DEBUG int getUserInput () { #ifdef ENABLE_DEBUG std::cerr << "getUserInput() called\n" ; #endif std::cout << "Enter a number: " ; int x{}; std::cin >> x; return x; } int main () { #ifdef ENABLE_DEBUG std::cerr << "main() called\n" ; #endif int x{ getUserInput () }; std::cout << "You entered: " << x << '\n' ; return 0 ; }
使用日志记录器(Logger) 现代常用的调试和错误记录方式,具体内容可以在网络上学习
类和对象的关系 类是模板 ,对象是根据这个模板创建出来的实例
类和结构体的关系 使用结构体完成一段功能程序:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct Node { string name; int age; void setValue (string name1,int age1) { name = name1; age = age1; } void show () { cout<<name<<" " <<age; } }node; int main () { string name; int age; cin>>name>>age; node.setValue (name,age); node.show (); return 0 ; }
将这段结构体改为类的形式:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class CNode { public : string name; int age; void setValue (string name1,int age1) ; void show () { cout<<name<<" " <<age; } }; void CNode::setValue (string name1,int age1) { name = name1; age = age1; } int main () { CNode node; string name; int age; cin>>name>>age; node.setValue (name,age); node.show (); return 0 ; }
简单来看C++中的类和结构体其实含义是相似的只是在表现形式上有所差别,但是其使用的思想是不太相同的。
类的权限
访问属性
属性
对象内部
对象外部
public
公有
可访问
可访问
protected
保护
可访问
不可访问
private
私有
可访问
不可访问
struct中所有行为和属性都是public 的(默认)。 C++中的class可以指定行为和属性的访问方式,默认是private。
对象的构造和析构 这就不得不提中文翻译的不便之处了,从英文意思来看很简单, 构造函数就是对对象进行基本的初始化 , 析构函数就是在对象销毁前 完成一些清理工作
构造函数 Constructor 基本语法 作用:
当对象被创建时自动调用,用于初始化对象的成员变量或执行其他初始化操作。 特点:
函数名必须和类名相同。
没有返回值(连 void 都不能写)。
可以有多个构造函数(函数重载),实现不同的初始化方式。
可以有默认参数,也可以写成 默认构造函数(无参数)。
访问权限必须是public
创建对象时只会自动调用一次,不能手动调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class CNode {private : string name; int age; public : CNode (){ name = "" ; age = -100 ; } CNode (string name1){ name = name1; age = 0 ; } void setValue (string name1,int age1) ; void show () ; }; int main () { CNode node; CNode node ("张三" ) ; ... }
注意:对构造函数进行重载的适合也遵从重载的要求:默认参数可能影响重载1 2 void f (int x, int y = 0 ) ;void f (int x) ;
初始化列表 基本语法:类名(形参列表):成员1(值1),成员2(值2)... {......}
示例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class CNode {private : string name; int age; public : CNode (){ name = "" ; age = -100 ; } CNode (string name1):name (name1),age (0 ){ } void setValue (string name1,int age1) ; void show () ; }; int main () { CNode node; CNode node ("张三" ) ; ... }
注意事项:
从表现上来看使用初始化列表和直接对变量进行赋值没有区别。但是如果需要初始化的成员是类 ,使用初始化列表调用的是成员类的拷贝构造函数 ,而赋值则是先创建成员类的对象(调用成员类的普通构造函数),然后再赋值
如果成员是常量(const)和引用,必须使用初始化列表,因为常量和引用只能在定义的时候初始化
析构函数 Destructor 作用:
当对象生命周期结束时自动调用,用于释放资源或执行清理操作(如释放内存、关闭文件等)。
特点:
函数名 类名前加 ~(波浪号)。
没有返回值,不能有参数,也不能重载。
对象销毁时自动调用,无需手动调用。
创建对象时只会自动调用一次,可以手动调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public : CNode (){ name.clear (); age = 0 ; } CNode (string name1){ name = name1; age = 0 ; } ~CNode (){ cout<<"调用析构函数" ; }
拷贝构造函数 用一个已存在对象创建一个新对象 时调用的构造函数 也就是说新对象,不由构造函数来构造,而是由拷贝构造函数来完成。拷贝构造函数的格式是固定的。
语法:1 2 3 4 5 6 7 class 类名{ 类名(const 类名 &形参名) { 拷贝构造体 } }
示例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Person {public : string name; Person (){ name.clear (); cout << "构造函数调用" << endl; } Person (const Person& other) { this ->name = "拷贝" +other.name; cout << "拷贝构造函数调用" << endl; } ~Person (){ } }; int main () { Person p; p.name = "张三" ; Person p2 = p; cout<<p.name<<" " <<p2. name; return 0 ; }
拷贝构造函数可以重载,也可以有默认参数
如果类中重载了拷贝构造函数却没有定义默认的拷贝构造函数,编译器也会提供默认的拷贝构造函数
浅拷贝和深拷贝
类型
拷贝内容
优点
缺点
浅拷贝
拷贝指针的值(只复制地址)
快
会导致重复释放、内存问题
深拷贝
拷贝指针指向的数据,重新开辟空间
安全、不互相影响
多一次开销
什么时候必须深拷贝? 类里有指针成员(尤其是 new 出来的资源) 必须写深拷贝,否则一定出 bug
简单来说,当类类中有指针成员的时候,使用浅拷贝会在拷贝赋值的时候将两个指针指向一片内存,当类A生命周期结束会释放这片内存。 如果之后拷贝的类也结束了这个类的指针就成了野指针,再去尝试释放当初指向的内存就会导致错误崩溃。 而C++中默认的拷贝构造函数使用的都是浅拷贝,所以当类中需要用到指针时,一定要手动写深拷贝的拷贝构造函数。
看下面代码示例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Person {public : int age; int * score; Person (int a, int s){ age = a; score = new int (s); } }; Person p1 (18 , 90 ) ;Person p2 = p1; Person (const Person& p){ age = p.age; score = new int (*p.score); }
注意事项
在构造函数后面加括号和参数不是调用构造函数,而是创建匿名对象 看下面这个例子:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class CNode {private : string name; int age; public : CNode (){ name = "" ; age = -100 ; } CNode (string name1){ CNode (); name = name1; } void setValue (string name1,int age1) ; void show () ; }; int main () { CNode node ("张三" ) ; ... }
如果使用上面这个构造方式会导致非常严重的问题,在传参的重载构造函数中我们调用了CNode(),如果在类中需要初始化的内容很多这样写,似乎可以减少代码量。但是事实上这并不是调用构造函数,而是创建了匿名对象,并没有对对象进行初始化 。如果在对象中使用了指针就会产生野指针 ,甚至导致程序崩溃。
下面两行代码有本质区别1 2 3 4 5 CNode node = CNode ("张三" ); CNode node; node = CNode ("张三" );
上面代码的方法2 其实调用了两次构造函数和析构函数
创建对象的方式 栈上创建 栈上创建对象(直接创建)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Person {public : string name; Person (string n) : name (n) { cout << "构造函数: " << name << endl; } ~Person () { cout << "析构函数: " << name << " 被调用" << endl; } }; int main () { Person p ("张三" ) ; cout << p.name << endl; }
堆上创建 堆上创建对象(使用 new 和 delete)1 2 3 4 5 6 7 8 9 10 11 12 #include <iostream> using namespace std;int main () { Person* p = new Person ("李四" ); cout << p->name << endl; delete p; }
两种方式的对比
对比项
栈对象
堆对象
内存位置
栈
堆
生命周期
随作用域结束自动销毁
程序员控制,通过 delete
内存管理
自动管理
需手动管理
访问方式
.
->(指针访问)
创建速度
快
慢一些
安全性
高
容易忘记释放,可能泄漏
使用场景
对象数量小、生命周期固定
对象数量大或生命周期不确定
绝大多数情况下,直接在栈上创建对象更好
只有在以下情况才建议用 new:
对象数量非常多,栈空间不够
对象需要跨作用域 或动态控制
对象需要在运行时动态创建和销毁
现代 C++ 更推荐使用 智能指针 ,避免因为忘记delete而导致的内存泄漏
const修饰成员函数 在成员函数中后加 const 关键字修饰即可 示例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Node { private : string name; int age; public : Node (){ name = "张三" ; age = 0 ; } void show () const ; }; void Node::show () const { cout<<name<<" " <<age<<endl; }
const修饰的成员函数 只能调用const修饰的成员函数 ,非cosnt修饰的成员函数 则不受限制
const修饰的对象 只能调用const修饰的成员函数 ,非cosnt修饰的对象 则不受限制
this指针 this 指针是 C++ 类中一个 隐式存在的指针 每一个成员函数里都带着它。 它指向当前对象本身。
使用 return 返回时使用 *this 返回当前对象
类的静态成员 作用:静态成员变量和静态成员函数可以在多个对象之间共享数据,比全局变量更加安全
本质:将类的成员声明为静态的,就可以把它和类的对象独立开来,或者说静态成员不属于对象而是只属于类的 ,整个程序中只储存一份
使用注意:
静态成员变量不会再创建对象的适合初始化,需要在程序的全局区用代码清晰的初始化
静态成员使用类名加::即可访问,不需要创建对象1 2 3 4 5 6 7 8 9 10 class Node { static int age; }; int Node::age; int main () { cout<<Node::age; ... }
总结: | 类型 | 属于 | 是否有 this | 所有对象是否共享 | | ——— | — | ———— | ———— | | 普通成员变量 | 对象 | 有 | 否 | | 静态成员变量 | 类 | 无 | 是 | | 普通成员函数 | 对象 | 有 | — | | 静态成员函数 | 类 | 无 | — |
友元 关键字: friend 产生原因: 如果要访问类的私有成员变量,可以通过调用类的public 成员函数,但是类的private 成员函数则无法访问。友元则提供了一种访问类的private 成员的方法。
友元的定义可以放在类中的任意位置
友元有三种:
友元全局函数1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Person { friend void showAge (Person& p) ; private : int age; public : Person (){} Person (int a):age (a){} }; void showAge (Person& p) { cout << p.age << endl; }
友元类1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class A { friend class B ; private : int x; public : A ():x (10 ){} }; class B {public : void f (A& a) { cout << a.x << endl; } };
注意事项:
友元关系不能被继承
友元关系是单向的,不具备交换性
友元成员函数 假设有两个类 A 和 B,我们希望 B 的某个成员函数能访问 A 的私有成员,而不是整个类 B 都可以访问:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class B ; class A {private : int x; public : A (int val) : x (val) {} friend void B::f (A& a) ; }; class B {public : void f (A& a) { cout << "访问 A 的私有成员 x = " << a.x << endl; } }; int main () { A a (100 ) ; B b; b.f (a); }
说明:
需要前向声明:由于 A 里要声明 B::f 为友元,必须提前告诉编译器 B 是一个类
friend 声明:写在类 A 内,声明 B::f 是 A 的友元成员函数
类的自动类型转换 概念: C++ 支持对象在某些情况下自动转换成其他类型,或者其他类型转换成对象 。
C++基础类型 → 类对象 示例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class A { int x; public : A (){} A (int val) : x (val) {} void show () { cout << x << endl; } }; int main () { A a = 10 ; a.show (); } int main () { A a1 (8 ) ; A a1 = A (8 ); A a1 = 8 A a1; a1 = 8 ; }
说明:
单参数构造函数 可以被用作 类型转换构造函数
禁止自动类型转换: 如果不想允许隐式转换(仍可以显示转换) ,可以在单参数构造函数前加 explicit1 2 3 4 5 6 7 8 9 10 11 class A { int x; public : explicit A (int val) : x(val) { } }; int main () { A a (10 ) ; }
类 → 其他类型 通过在类中定义 operator Type()函数实现。
注意:
在实际开发中使用operator进行类型转换不如使用自己手动编写的类型转换函数 ,因为隐式的转换有时候可能会导致错误和二义性 ,反而不利于代码维护和排查错误
示例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class A { int x; string y; double z; public : A (int val) : x (val) {} operator int () { return x; } operator string () { return y; } operator double () {return z; } }; int main () { A a; int n = a; string y = string (A); double z = double (A); }