1. 内联 inline
c 里面的宏嵌入是源码嵌入,内联是二进制嵌入,二进制代替多次调用;
内联一般用于多次调用的小函数,用空间换时间;
注意:递归函数无法作内联,递归是自己调自己,所以无法自己嵌自己;
通过 inline 关键字,可以建议编译器对函数进行内联,但是仅仅是建议,编译器可以不执行;
inline void foo(int x) {...}
现在的编译器会自己判断是否需要内联;
2. 动态内存分配 new/delete(操作符)
c 里面用下面几个函数动态分配内存,位于堆里面,c++ 同样可以使用,需要包含头文件 #include <cstdlib>
malloc(不清零)/calloc(清零)/realloc/free
int *p = (int *)malloc(1 * sizeof(int)); // c++里不推荐使用
new 和 malloc 区别:
1)new 是运算符,malloc 是函数;
2)new 最终还是调用 malloc 分配内存;
3)malloc 仅仅是分配内存,new 还调用构造函数;
c++里主要用下面函数进行内存分配和释放:(不用包含任何头文件)
new/delete :对单个变量进行内存分配/释放,上面那段 malloc 代码可以写为:
int *p = new int;
delete p;
p = NULL;
也可以如下初始化:(有待验证)
int *p = new int (56); // 对p进行赋初值
下面两种变量初始化方式都正确:
int i = 10;
int i(10);
一般情况下,我们 delete 完了,就要把指针清零置空;
int *p = new int;
delete p;
*p = 2000; // 这种情况下,p属于野指针,无法确定结果,属于未定义类型;
int *p = new int;
delete p;
p = NULL;
*p = 2000; // 这种情况下,p为零,结果确定,返回段错误;
对数组进行内存分配/释放:new[]/delete[]:
p = new int [10];
delete[] p; // 如果此处不带[ ],那么结果不确定,属于未定义;多维数组也用这条语句释放,不用多加[ ]
p = NULL;
注意:
delete[] p; 相当于: delete (p-4);
记着,这里 p 前面有 4 个字节用于存放数组大小;
3. 定位分配:在指定位置分配内存空间;(了解)
并不是所有 new 分配出来的空间都在堆里面;
char buf[4] = {0x12, 0x34, 0x99, 0x23}; // buf里面放了四个数,buf 位于栈里面
int *p = new (buf) int; // 在 buf 里面分配一个 int 空间给 p,所以这个空间在栈里面
delete p; // error,段错误,栈不需要释放
4. 引用即别名;
int a = 20;
int &b = a; // 引用,b就是a的别名
b = 10; // 此时 a 值为10
引用必须初始化,且非常引用必须用变量初始化;
只有常引用可以用常量初始化;
一旦初始化,就不能再引用其他变量;
int &b; // error
int &b = 1000; // error
const int &b = 100; // ok
引用是在底层其实就是用指针来实现的。
c++ 和 c 相比,就多加了两个变量,一个是布尔,另一个是引用;
5. 引用应用场景:
1)引用型参数:
a.修改实参
b.避免拷贝
通过 const 可以防止在函数中意外地修改实参的值;
同时还可以接收拥有常属性的实参;如下:
void foo(const int &a) {...} // 增加 const,foo 的实参范围变大了,既可以传变量,也可以传常量
// 如果 foo 函数体内需要改变 a 的值,我们则不能加 const
int main() { foo(100); }
2)引用型返回值:
int data = 100; // data必须是全局变量(原因见下面)
int &foo() { return data; } // 返回引用,相当于返回 data 本身;
int main() { foo() = 200; } // 如果foo不是引用,则无法把 200 返回给data,同时data要是全局变量才行
从一个函数中返回引用,往往是为了将该函数的返回值作为左值使用;
但是,一定要保证函数所返回的引用的目标在该函数返回以后依然有定义,否则将导致不确定后果;
也就是说,不要返回局部变量;
除非局部变量是静态的,或是在动态内存中分配的;
可以返回全局、静态、成员变量的引用,也可以返回引用型行参;
int &foo() { int a; return a; } // 无效,a是局部变量
int &foo(int a) { return a; } // 无效,a是执行型行参
int &foo(int &a) { return a; } // 有效,a是引用型行参
函数的行参可以是引用,而通过引用传递参数,称之为引用传递;
int &i = foo(); // ok
int j = foo(); // ok
在引用传递时,经常使用 const 来保护引用参数的传递;
在C++中,尽量使用引用传递参数,尽量使用引用代替指针;
example:
int& foo (const int& a) { return a; }
int main () { foo (100); }
编译提示错误:传进去的是 const a,那么返回的应该也是 const 型;
在函数前面加上 const 就可以了:
const int& foo (const int& a) { return a; }
tips:这个例题实际意义不大,因为函数返回类型为引用,目的是为了做左值(可以修改);
但这里又加了 const 属性,让其不能做左值,矛盾;
所以这个例题只是说明,如果 return const 类型,那么函数返回值也必须是 const;
6. 引用的本质就是指针,很多场合下是可以互换的;
在高级语言 c++ 层面上,他们还是有区别的,如下:
1)指针是实体变量,但是引用不是实体变量,只是一个别名;
int &a = i; sizeof(a); // a 即(int)i 的大小 4
double &b = f; sizeof(b); // b 即(double)f 的大小 8
2)指针是实体,可以不初始化(但最好初始化);引用必须初始化;
3)指针的目标可以修改,但是引用的目标不能修改;
int&a = i; ==> int* const a = &i; // a 不能再指向别的变量
4)可以定义指针的指针,也就是二级指针;但是不可以定义引用的指针;
int &r = a; int&* p = &r; // 不对,r不是实体,没有地址,也就没有指针
5)可以定义指针的引用,但是不可以定义引用的引用;
int a; int *p = &a; int*& q = p; // OK
int a; int &r = a; int&& s = r; // ERROR,但是 int &s = r; 是正确的;
6)可以定义指针的数组,但是不能定义引用的数组;
int a, b, c; int *parr[] = {&a, &b, &c}; // OK
int a, b, c; int &rarr[] = {a, b, c}; // ERROR
可以定义数组的引用:
int arr[] = {1, 3, 5}; int (&arr1)[3] = arr; // OK
举例 1
int a = 100, b = 200;
int *p1 = &a, *p2 = &b;
p1 = p2; // p1 指向 b 地址,但是 a 的值不变
int &r1 = a, &r2 = b;
r1 = r2; // 引用改变原对象值,此时 a = 200;
7. 显示类型转换运算符
c里面叫做强制类型转换;
c++里面分为以下几种类型转换:
1)静态类型转换
static_cast < 目标类型 >( 源类型变量 )
转换时作静态检查,即在编译时进行;
void * 到其他指针的转换;
如果在目标类型和源类型之间,某一个方向上可以作隐式类型转换,那么两个方向上都可以作静态类型转换;
如果在两个方向上都不能作隐式类型转换,那么在任意一个方向上也不能作静态类型转换;
int *p1 = ...; void *p2 = p1;
p1 = static_cast<int *>(p2);
如果存在从源类型到目标类型的自定义转换规则,那么也可以使用静态类型转换;(了解,后面再讲)
2)动态类型转换(后期多态部分讲)
dynamic_cast < 目标类型 >( 源类型变量 )
主要用于具有多态性的父子类指针或引用之间;
3)常类型转换
const_cast < 目标类型 >( 源类型变量 )
给一个拥有const属性的指针或引用去掉常属性;
int a = 100;
const int *p1 = &a; // ok,常指针可以指非常;反过来不行,看下面例子
*p1 = 200; // error, p1不能改
int *p2 = p1; // error,p1是常属性,p2不是,两者属性不匹配
int *p2 = const_cast<int *>(p1);
*p2 = 200; // ok,p2被去常了
const int b = 100;
int *p3 = &b; // error,b是const型,不能赋值给非常p3
常量和拥有常属性的变量是不一样的;后者在特定情况下可以改变,而前者无法改变;
c++ 编译器特有的常量优化:
const int a = 100;
int *p1 = const_cast<int *>(&a);
*p1 = 200;
cout << a; // 100, 编译器默认为凡是有a的地方,都提前换为100 :cout << 100;
cout << *p1; // 200
上面给 a 加了 const 属性,如果给 a 再加上 volatile 后,const volatile int a = 100;
编译器就不对 a 进行常量优化,那么最后 a 的值就是 200;(和硬件相关的时候用到)
注意:const_cast 只是去常,不能进行类型转换,也就是说,不能写为:int *p2 = const_cast<double *>(p1);
或者如果 p1 为 double 型,也不能写成:int *p2 = const_cast<int *>(p1);
问:关键字 volatile 有什么含意 并给出三个不同的例子 ?
答:一个定义为 volatile 的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。
精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
下面是 volatile 变量的几个例子:
1) 并行设备的硬件寄存器(如:状态寄存器)
2) 一个中断服务子程序中会访问到的非自动变量 (Non-automatic variables)
3) 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分 C 程序员和嵌入式系统程序员的最基本的问题。
嵌入式系统程序员经常同硬件、中断、RTOS 等等打交道,所用这些都要求 volatile 变量。
不懂得 volatile 内容将会带来灾难。
假设被面试者正确地回答了这个问题(嗯,怀疑这否会是这样),我将稍微深究一下,
看一下这家伙是不是真正懂得 volatile 完全的重要性。
1) 一个参数既可以是 const 还可以是 volatile 吗?解释为什么。
2) 一个指针可以是 volatile 吗?解释为什么。
3) 下面的函数有什么错误:
int square (volatile int *ptr) {
return *ptr * *ptr;
}
下面是答案:
1) 是的。一个例子是只读的状态寄存器。它是 volatile 因为它可能被意想不到地改变。
它是 const 因为程序不应该试图去修改它。
2) 是的。尽管这并不很常见。一个例子是当一个中服务子程序修改一个指向一个 buffer 的指针时。
3) 这段代码有个恶作剧。这段代码的目的是用来返回指针 *ptr 指向值的平方,
但是,由于 *ptr 指向一个 volatile 型参数,编译器将产生类似下面的代码:
int square (volatile int *ptr) {
int a, b;
a = *ptr;
b = *ptr;
return a * b;
}
由于 *ptr 的值可能被意想不到地该变,因此 a 和 b 可能是不同的。
结果,这段代码可能返回不是你所期望的平方值!正确的代码如下:
long square (volatile int *ptr) {
int a;
a = *ptr;
return a * a;
}
4)重解释类型转换
reinterpret_cast < 目标类型 >( 源类型变量 )
在不同类型的指针或引用之间作类型转换,以及在指针和整型之间作类型转换;
可以强转任何类型的指针;
把整数强转成指针;或者把指针强转成整数;
int i = 0x12345678;
char *p = reinterpret_cast<char *>(&i); // 把int看作4个char
for (size_t i = 0; i < 4; ++i)
cout << hex << (int)p[i] << ' '; // 输出:78 56 34 12(小端)
float *p = reinterpret_cast<float *>(&i);
cout << *p << endl;
void *v = reinterpret_cast<void *>(i);
cout << v << endl;
5)目标类型变量 = 目标类型(源类型变量); // c里面强制类型转换是:目标类型变量 = (目标类型)源类型变量;
int a = int (3.14); // a = 3;
char c = 'a';
int n = (int)c; // c还是char型,临时生成一个临时变量/匿名变量,强制为int型,值和char c是一样的
(int)c = 100; // erro,匿名变量都是右值,不能被赋值;
所有的匿名变量都是右值,如果要引用它,需要带有 const
void foo(int &a){...} // void foo(const int &a){...}
char c = 'a';
foo(c); // error,类型不一致
foo((int)c); // 实参是int型临时变量,是右值,行参是左值引用,如果在行参里面加const就对了
8. c++ 之父的建议:
1)少用宏,多用 const、enum、inline;
#define PI 3.14
const double PI = 3.14;
#define ERROR -1
enum{ ERROR = -1 };
#define max(a, b) ((a) > (b) ? (a) : (b))
inline int double max(double a, double b){ return a > b ? a : b; }
对于单纯常量,最好以 const 或 enum 替换;
对于形似函数的宏,最好用 inline 函数替换;
2)变量随用随声明,同时初始化;
3)少用 malloc/free,多用 new/delete
4)少用c风格的强制类型转换,多用类型转换运算符;
5)少用c风格的字符串,"\n,\0",多用string;
6)少用数组;
7)树立面向对象的编程思想;
问:请说出 const 与 #define 相比,有何优点 ?
答:const 作用:定义常量、修饰函数参数、修饰函数返回值三个作用。
被 const 修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
1)const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。
而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
2)有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
9. 对象---面向对象程序设计(OOP)
1)程序就是一组对象,对象之间通过消息交换信息;
2)类就是对对象的描述和抽象,对象就是对类的具体化和实例化;
3)c++中对象其实就是一个变量,在计算机中是一片内存区域;
4)类通过属性和行为两方面对对象进行抽象;
c++中,对象属性就是成员变量;对象行为就是成员函数;
判断一个程序是否是面向对象,判断依据看其是否具有:封装,继承,多态;
10. 面向对象程序设计(OOP)学习过程:
1)至少掌握一种OOP变成语言;
2)精通一种面向对象的元语言,如UML;
3)研究设计模式,如GOF;
11. 类的定义:
class 类名 {
类型 成员变量名; // 不能在此初始化
返回类型 成员函数名(行参表){函数体}; // 通过函数初始化成员变量,一般是构造函数
}; // 末尾的分号别忘记
类是创建对象的模型;
c++中类就是用户自定义的数据类型;
成员变量如果不初始化,结果不确定;
int --- 随机数
char --- \0 或者 NULL
c++里,变量有成员、局部和全局之分;
函数有成员函数和全局函数之分;没有局部函数;
12. 访问控制属性:
public: 公有成员,大家都可以访问;
private: 私有成员,只有自己可以访问;(缺省属性)
protected: 保护成员,自己和自己的子类可以访问;
class 类的缺省访控属性是 private,struct 结构的缺省访控属性是 public,用于和c兼容;
class Student{
public:
string m_name;
int m_age;
void eat(const string &food)
{ cout << food << endl; }
};
int main()
{
Student student = {"zhangfei", 23};
Student student; // student就是实例化对象
student.m_name = "zhangfei";
student.m_age = 23;
student.eat("beer");
return 0;
}
c 里面的宏嵌入是源码嵌入,内联是二进制嵌入,二进制代替多次调用;
内联一般用于多次调用的小函数,用空间换时间;
注意:递归函数无法作内联,递归是自己调自己,所以无法自己嵌自己;
通过 inline 关键字,可以建议编译器对函数进行内联,但是仅仅是建议,编译器可以不执行;
inline void foo(int x) {...}
现在的编译器会自己判断是否需要内联;
2. 动态内存分配 new/delete(操作符)
c 里面用下面几个函数动态分配内存,位于堆里面,c++ 同样可以使用,需要包含头文件 #include <cstdlib>
malloc(不清零)/calloc(清零)/realloc/free
int *p = (int *)malloc(1 * sizeof(int)); // c++里不推荐使用
new 和 malloc 区别:
1)new 是运算符,malloc 是函数;
2)new 最终还是调用 malloc 分配内存;
3)malloc 仅仅是分配内存,new 还调用构造函数;
c++里主要用下面函数进行内存分配和释放:(不用包含任何头文件)
new/delete :对单个变量进行内存分配/释放,上面那段 malloc 代码可以写为:
int *p = new int;
delete p;
p = NULL;
也可以如下初始化:(有待验证)
int *p = new int (56); // 对p进行赋初值
下面两种变量初始化方式都正确:
int i = 10;
int i(10);
一般情况下,我们 delete 完了,就要把指针清零置空;
int *p = new int;
delete p;
*p = 2000; // 这种情况下,p属于野指针,无法确定结果,属于未定义类型;
int *p = new int;
delete p;
p = NULL;
*p = 2000; // 这种情况下,p为零,结果确定,返回段错误;
对数组进行内存分配/释放:new[]/delete[]:
p = new int [10];
delete[] p; // 如果此处不带[ ],那么结果不确定,属于未定义;多维数组也用这条语句释放,不用多加[ ]
p = NULL;
注意:
delete[] p; 相当于: delete (p-4);
记着,这里 p 前面有 4 个字节用于存放数组大小;
3. 定位分配:在指定位置分配内存空间;(了解)
并不是所有 new 分配出来的空间都在堆里面;
char buf[4] = {0x12, 0x34, 0x99, 0x23}; // buf里面放了四个数,buf 位于栈里面
int *p = new (buf) int; // 在 buf 里面分配一个 int 空间给 p,所以这个空间在栈里面
delete p; // error,段错误,栈不需要释放
4. 引用即别名;
int a = 20;
int &b = a; // 引用,b就是a的别名
b = 10; // 此时 a 值为10
引用必须初始化,且非常引用必须用变量初始化;
只有常引用可以用常量初始化;
一旦初始化,就不能再引用其他变量;
int &b; // error
int &b = 1000; // error
const int &b = 100; // ok
引用是在底层其实就是用指针来实现的。
c++ 和 c 相比,就多加了两个变量,一个是布尔,另一个是引用;
5. 引用应用场景:
1)引用型参数:
a.修改实参
b.避免拷贝
通过 const 可以防止在函数中意外地修改实参的值;
同时还可以接收拥有常属性的实参;如下:
void foo(const int &a) {...} // 增加 const,foo 的实参范围变大了,既可以传变量,也可以传常量
// 如果 foo 函数体内需要改变 a 的值,我们则不能加 const
int main() { foo(100); }
2)引用型返回值:
int data = 100; // data必须是全局变量(原因见下面)
int &foo() { return data; } // 返回引用,相当于返回 data 本身;
int main() { foo() = 200; } // 如果foo不是引用,则无法把 200 返回给data,同时data要是全局变量才行
从一个函数中返回引用,往往是为了将该函数的返回值作为左值使用;
但是,一定要保证函数所返回的引用的目标在该函数返回以后依然有定义,否则将导致不确定后果;
也就是说,不要返回局部变量;
除非局部变量是静态的,或是在动态内存中分配的;
可以返回全局、静态、成员变量的引用,也可以返回引用型行参;
int &foo() { int a; return a; } // 无效,a是局部变量
int &foo(int a) { return a; } // 无效,a是执行型行参
int &foo(int &a) { return a; } // 有效,a是引用型行参
函数的行参可以是引用,而通过引用传递参数,称之为引用传递;
int &i = foo(); // ok
int j = foo(); // ok
在引用传递时,经常使用 const 来保护引用参数的传递;
在C++中,尽量使用引用传递参数,尽量使用引用代替指针;
example:
int& foo (const int& a) { return a; }
int main () { foo (100); }
编译提示错误:传进去的是 const a,那么返回的应该也是 const 型;
在函数前面加上 const 就可以了:
const int& foo (const int& a) { return a; }
tips:这个例题实际意义不大,因为函数返回类型为引用,目的是为了做左值(可以修改);
但这里又加了 const 属性,让其不能做左值,矛盾;
所以这个例题只是说明,如果 return const 类型,那么函数返回值也必须是 const;
6. 引用的本质就是指针,很多场合下是可以互换的;
在高级语言 c++ 层面上,他们还是有区别的,如下:
1)指针是实体变量,但是引用不是实体变量,只是一个别名;
int &a = i; sizeof(a); // a 即(int)i 的大小 4
double &b = f; sizeof(b); // b 即(double)f 的大小 8
2)指针是实体,可以不初始化(但最好初始化);引用必须初始化;
3)指针的目标可以修改,但是引用的目标不能修改;
int&a = i; ==> int* const a = &i; // a 不能再指向别的变量
4)可以定义指针的指针,也就是二级指针;但是不可以定义引用的指针;
int &r = a; int&* p = &r; // 不对,r不是实体,没有地址,也就没有指针
5)可以定义指针的引用,但是不可以定义引用的引用;
int a; int *p = &a; int*& q = p; // OK
int a; int &r = a; int&& s = r; // ERROR,但是 int &s = r; 是正确的;
6)可以定义指针的数组,但是不能定义引用的数组;
int a, b, c; int *parr[] = {&a, &b, &c}; // OK
int a, b, c; int &rarr[] = {a, b, c}; // ERROR
可以定义数组的引用:
int arr[] = {1, 3, 5}; int (&arr1)[3] = arr; // OK
举例 1
int a = 100, b = 200;
int *p1 = &a, *p2 = &b;
p1 = p2; // p1 指向 b 地址,但是 a 的值不变
int &r1 = a, &r2 = b;
r1 = r2; // 引用改变原对象值,此时 a = 200;
7. 显示类型转换运算符
c里面叫做强制类型转换;
c++里面分为以下几种类型转换:
1)静态类型转换
static_cast < 目标类型 >( 源类型变量 )
转换时作静态检查,即在编译时进行;
void * 到其他指针的转换;
如果在目标类型和源类型之间,某一个方向上可以作隐式类型转换,那么两个方向上都可以作静态类型转换;
如果在两个方向上都不能作隐式类型转换,那么在任意一个方向上也不能作静态类型转换;
int *p1 = ...; void *p2 = p1;
p1 = static_cast<int *>(p2);
如果存在从源类型到目标类型的自定义转换规则,那么也可以使用静态类型转换;(了解,后面再讲)
2)动态类型转换(后期多态部分讲)
dynamic_cast < 目标类型 >( 源类型变量 )
主要用于具有多态性的父子类指针或引用之间;
3)常类型转换
const_cast < 目标类型 >( 源类型变量 )
给一个拥有const属性的指针或引用去掉常属性;
int a = 100;
const int *p1 = &a; // ok,常指针可以指非常;反过来不行,看下面例子
*p1 = 200; // error, p1不能改
int *p2 = p1; // error,p1是常属性,p2不是,两者属性不匹配
int *p2 = const_cast<int *>(p1);
*p2 = 200; // ok,p2被去常了
const int b = 100;
int *p3 = &b; // error,b是const型,不能赋值给非常p3
常量和拥有常属性的变量是不一样的;后者在特定情况下可以改变,而前者无法改变;
c++ 编译器特有的常量优化:
const int a = 100;
int *p1 = const_cast<int *>(&a);
*p1 = 200;
cout << a; // 100, 编译器默认为凡是有a的地方,都提前换为100 :cout << 100;
cout << *p1; // 200
上面给 a 加了 const 属性,如果给 a 再加上 volatile 后,const volatile int a = 100;
编译器就不对 a 进行常量优化,那么最后 a 的值就是 200;(和硬件相关的时候用到)
注意:const_cast 只是去常,不能进行类型转换,也就是说,不能写为:int *p2 = const_cast<double *>(p1);
或者如果 p1 为 double 型,也不能写成:int *p2 = const_cast<int *>(p1);
问:关键字 volatile 有什么含意 并给出三个不同的例子 ?
答:一个定义为 volatile 的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。
精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
下面是 volatile 变量的几个例子:
1) 并行设备的硬件寄存器(如:状态寄存器)
2) 一个中断服务子程序中会访问到的非自动变量 (Non-automatic variables)
3) 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分 C 程序员和嵌入式系统程序员的最基本的问题。
嵌入式系统程序员经常同硬件、中断、RTOS 等等打交道,所用这些都要求 volatile 变量。
不懂得 volatile 内容将会带来灾难。
假设被面试者正确地回答了这个问题(嗯,怀疑这否会是这样),我将稍微深究一下,
看一下这家伙是不是真正懂得 volatile 完全的重要性。
1) 一个参数既可以是 const 还可以是 volatile 吗?解释为什么。
2) 一个指针可以是 volatile 吗?解释为什么。
3) 下面的函数有什么错误:
int square (volatile int *ptr) {
return *ptr * *ptr;
}
下面是答案:
1) 是的。一个例子是只读的状态寄存器。它是 volatile 因为它可能被意想不到地改变。
它是 const 因为程序不应该试图去修改它。
2) 是的。尽管这并不很常见。一个例子是当一个中服务子程序修改一个指向一个 buffer 的指针时。
3) 这段代码有个恶作剧。这段代码的目的是用来返回指针 *ptr 指向值的平方,
但是,由于 *ptr 指向一个 volatile 型参数,编译器将产生类似下面的代码:
int square (volatile int *ptr) {
int a, b;
a = *ptr;
b = *ptr;
return a * b;
}
由于 *ptr 的值可能被意想不到地该变,因此 a 和 b 可能是不同的。
结果,这段代码可能返回不是你所期望的平方值!正确的代码如下:
long square (volatile int *ptr) {
int a;
a = *ptr;
return a * a;
}
4)重解释类型转换
reinterpret_cast < 目标类型 >( 源类型变量 )
在不同类型的指针或引用之间作类型转换,以及在指针和整型之间作类型转换;
可以强转任何类型的指针;
把整数强转成指针;或者把指针强转成整数;
int i = 0x12345678;
char *p = reinterpret_cast<char *>(&i); // 把int看作4个char
for (size_t i = 0; i < 4; ++i)
cout << hex << (int)p[i] << ' '; // 输出:78 56 34 12(小端)
float *p = reinterpret_cast<float *>(&i);
cout << *p << endl;
void *v = reinterpret_cast<void *>(i);
cout << v << endl;
5)目标类型变量 = 目标类型(源类型变量); // c里面强制类型转换是:目标类型变量 = (目标类型)源类型变量;
int a = int (3.14); // a = 3;
char c = 'a';
int n = (int)c; // c还是char型,临时生成一个临时变量/匿名变量,强制为int型,值和char c是一样的
(int)c = 100; // erro,匿名变量都是右值,不能被赋值;
所有的匿名变量都是右值,如果要引用它,需要带有 const
void foo(int &a){...} // void foo(const int &a){...}
char c = 'a';
foo(c); // error,类型不一致
foo((int)c); // 实参是int型临时变量,是右值,行参是左值引用,如果在行参里面加const就对了
8. c++ 之父的建议:
1)少用宏,多用 const、enum、inline;
#define PI 3.14
const double PI = 3.14;
#define ERROR -1
enum{ ERROR = -1 };
#define max(a, b) ((a) > (b) ? (a) : (b))
inline int double max(double a, double b){ return a > b ? a : b; }
对于单纯常量,最好以 const 或 enum 替换;
对于形似函数的宏,最好用 inline 函数替换;
2)变量随用随声明,同时初始化;
3)少用 malloc/free,多用 new/delete
4)少用c风格的强制类型转换,多用类型转换运算符;
5)少用c风格的字符串,"\n,\0",多用string;
6)少用数组;
7)树立面向对象的编程思想;
问:请说出 const 与 #define 相比,有何优点 ?
答:const 作用:定义常量、修饰函数参数、修饰函数返回值三个作用。
被 const 修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
1)const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。
而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
2)有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
9. 对象---面向对象程序设计(OOP)
1)程序就是一组对象,对象之间通过消息交换信息;
2)类就是对对象的描述和抽象,对象就是对类的具体化和实例化;
3)c++中对象其实就是一个变量,在计算机中是一片内存区域;
4)类通过属性和行为两方面对对象进行抽象;
c++中,对象属性就是成员变量;对象行为就是成员函数;
判断一个程序是否是面向对象,判断依据看其是否具有:封装,继承,多态;
10. 面向对象程序设计(OOP)学习过程:
1)至少掌握一种OOP变成语言;
2)精通一种面向对象的元语言,如UML;
3)研究设计模式,如GOF;
11. 类的定义:
class 类名 {
类型 成员变量名; // 不能在此初始化
返回类型 成员函数名(行参表){函数体}; // 通过函数初始化成员变量,一般是构造函数
}; // 末尾的分号别忘记
类是创建对象的模型;
c++中类就是用户自定义的数据类型;
成员变量如果不初始化,结果不确定;
int --- 随机数
char --- \0 或者 NULL
c++里,变量有成员、局部和全局之分;
函数有成员函数和全局函数之分;没有局部函数;
12. 访问控制属性:
public: 公有成员,大家都可以访问;
private: 私有成员,只有自己可以访问;(缺省属性)
protected: 保护成员,自己和自己的子类可以访问;
class 类的缺省访控属性是 private,struct 结构的缺省访控属性是 public,用于和c兼容;
class Student{
public:
string m_name;
int m_age;
void eat(const string &food)
{ cout << food << endl; }
};
int main()
{
Student student = {"zhangfei", 23};
Student student; // student就是实例化对象
student.m_name = "zhangfei";
student.m_age = 23;
student.eat("beer");
return 0;
}