并不是全面覆盖,只是把自己认为的细节部分记录了下来,一部分来自于国嵌的c++视频,另一部分来自于c++ primer plus 。本人属于新手,什么都不懂,如果有错误请多多指教。
首先一个区别在我学完c++后与c语言弄混的地方,就是在c++中,可以在用到变量的时候再进行定义,而c语言就必须全部在开始的时候定义出。
for(int i=0; i<10; i++);
在c语言中对于register变量,是不能获得其地址的,但是c++中却可以,但是当取得register变量的地址后,register对变量的生命变得无效了。
在c语言中可以定义多个同名的全局变量,虽然定义了多个同名变量,但是都是同一个地址,就是说都是一样的;这个在c++中就不可以实现了。
对于const变量,在c语言中,大家多知道是伪常量,但是在c++中就变成了真正的常量,已经进入了符号表。
const int a = 10;
int *p = &a;
printf("%d\n", a);
*p = 1;
printf("%d\n", a);
这个在c语言中 a的值就被改变了,但是在c++中这样就不能编译过,因为a为常量,怎么能将常量赋值给无任何保护的(就是const)的指针呢??
const int a = 10;
int *p = (int *)&a;
printf("%d\n", a);
*p = 1;
printf("%d\n", a);
改为上边的方法,就可以编译通过了,但是最后的结果a的值两次都一样,都没有改变。 事实上在c++中,如果发现对const变量使用了extern或者&操作符,那么给常量分配地址,但是这个地址只是随便分配的,并不会使用,就更甭提修改这个const这个变量了。
const int a = 10;
const int b = 1;
int p[a+b];
像上边的代码,在c语言编译器中就不可以通过编译,因为不是真正的常量,但是在c++中就可以了,因为在c++中是真正的常量。
对于结构体:
struct Node
{
int a;
int b;
};
在c语言中如果想定义一个结构体变量,必须是这样子:struct Node n;
但是在c++中,会认为Node是一个新的变量,可以直接定义 Node n;
在c语言中并没有布尔类型,但是在c++中有了布尔类型,并且可取的值只有true和false,这里有个事情要注意,就是如果作为局部变量,那么一开始的时候的值 不会是正常的值,所以最好在定义的时候对其进行初始化,然后想说的是,对bool值的运算, 例如
bool a = true;
a++;
这里初始化之后,a的值是1,虽然进行了++运算,但是bool值只有两种,一种是0,一种是1.但是要注意,对于布尔型变量,如果当前的值为false 时, 对他进行减一,那么这个变量就会变成true了,因为正常的时候减一变成了负数,负数是非零的,所以就是true了。这个需要注意。但是当时true的时候,进行加一,就会一直是1了。
然后是关于sizeof(a), 这里布尔型的正常是占一个字节,但是有一些编译器会将连续的布尔型变量,放到一个字节里去,所以最后一个布尔型变量会占一个比特。
对于三目运算符来说,在c语言和c++中也是有区别的:区别不在于运算方式,而是返回类型上,在c语言中返回的是值,而在c++中返回的是变量的本身。
例如:
((a > b) ? a : b = 3) = 2;
printf("%d %d\n", a, b);
这个意思是如果a>b那么将a赋值为2,否则将b赋值为2.在c语言中不能通过编译,因为返回的这个变量的值,所以不能给一个值赋值啊?就像 2 =2, 这种绝对是不合法的。
引用:一个已经定义变量的别名,引用的话,必须类型一样。但是有一种方法可以让一个不同的类型引用编译通过,那就是通过地址呀:
int a = 1;
char &b = *(char *)&a;
b = 'a';
printf("%d %c\n", a, b);
不过这样做貌似没什么意义,只是突然想出来的,估计是没什么意义,突然感觉在和编译器玩捉迷藏的感觉。
如果是直接将a进行强制转换成char类型是不可以通过编译的。
继续说,引用在定义的时候就要进行初始化,但是只有当为函数参数的时候,可以不用初始化。一旦初始化完成之后,就不能改成别的变量的引用了。
当const 修饰一个引用的时候,只是把这个引用当成了只读变量,但是可以通过指针进行修改:
int a = 1;
const int &b = a;
int *p = (int *)&b;
*p = 10;
printf("%d %d\n", a, b);
a = 2;
printf("%d %d\n", a, b);
但是const引用的也是一个被const修饰过的变量那就没戏了。
其实在c++中引用就是一个指针 type& name = type* const name;
所以:
struct IRT
{
int &a;
int &b;
};
struct CHAR
{
char &a;
char &b;
};
这两个所占的字节都是一样的,都是8个字节。
int a = 10;
int *p = &a;
int &b = *p;//此时b已经变成了a的引用了
int c = 1;
p = &c; //虽然这里p只向了c 但是b仍然为a的引用
对于函数参数来说如果生命成了const 类型,那么可以接受const 和 非const的两种类型,如果参数没有声明成const 的话,那么只能接受非const了。
所以像char *p = "hello world";这种编译器就会提示出现问题,因为hell....是一个常量不能被修改的,但是p呢,是一个可以修改它指向的值得,这个很不安全;
对于非常大量的数据来说,如果返回值和参数都是引用的话,就非常高效了,因为可以避免生成临时变量并且拷贝到临时变量所需的时间了。
对于inline函数,必须放在函数定义的地方,或者是声明的地方(否则不能变成内敛函数)并且在c++中inline函数不一定最后真的变成了内敛函数,
内敛函数可以减少函数调用的时间。虽然inline有类似宏的方法,但是实际上还全部都是函数的特性。(函数参数也是按值传递,值也是可以强制转换的,
内联函数的几个限制:
1:不能存在任何形式的循环; 5:内敛声明必须在调用之前
2:不能存在过多的条件判断;
3:函数体不能庞大;
4:不能对函数取址(即函数指针)
关于函数的默认参数:在生命的时候或者是在定义的时候指定,只能是其中一个;
规则:在定义的时候:只要一个参数提供了默认参数,那么后面的参数就都要提供默认的值;
在调用的时候:其中一个采用了默认参数值,那么之后的就都必须采用默认值;
占位数:只声明类型,而不指定变量名
int mul(int a, int b, int)
{
}
这里第三个是不指定的,但是在调用的时候需要填写。
占位符的作用:为以后的扩展留下一条线索;
兼容c中不规范的做法;
关于函数重载:同一个函数名定义不同的函数;
满足要求:(至少一个不一样)
1:参数个数不同;
2:参数类型;
3:参数顺序不同;
(这里要注意一个问题,就是如果只有返回类型不同,而参数类表一样,那么编译器会报错,认为是一个函数)
重载函数可以返回类型不同,但是不要只有返回类型不同;
重载函数会有二义性的问题:
例如当默认参数遇到函数重载的问题:
void fun(int a, int b, int c=0)
{
cout<<"默认参数"<<endl;
}
void fun(int a, int b)
{
cout<<"无默认参数"<<endl;
}
在调用的时候:
int a = 0;
int b = 1;
fun(a, b);
这样就会产生二义性,因为不能确定应该调用哪个?
函数重载与函数指针:函数重载根据参数列表
但是函数指针就严格匹配指针函数类型;
void fun(int a)
{
}
void fun(int &a)
{
}
这两个函数在调用的时候如果直接传递一个int类型的变量也会出现二义性;
对于new和delete这两个函数没有什么,但是c语言中有malloc 和 free 这两个函数,一定要配对使用,因为对于用new申请的简单的数据类型确实可以使用free来进行释放,但是对于类就不可以了,因为free不会调用析构函数。
对于强制转换,在c++中升级了一下:
1、static_cast 2、const_cast 3、dynamic_cast 4、reinterpret_cast
这四种类型转换,
1:用于基本类型间的转换,但不能用于基本类型指针间的转换,用于有继承关系类对象之间的转换和类指针之间的转换
static_cast是编译期进行转换的,无法在运行时检测类型,所以类类型之间的转换可能存在风险。
2:去除变量const属性
const int& j = 1;//这样做,j就是一个只读变量,而不是真正的常量所以y的值会改变
int& k = const_cast<int&>(j);
const int x = 2;//这里的x是真正的常量
int& y = const_cast<int&>(x);//会临时分配一个内存给y 所以x值不会改变
k = 5;
printf("k = %d\n", k);
printf("j = %d\n", j);
y = 8;
printf("x = %d\n", x);
printf("y = %d\n", y);
printf("&x = %p\n", &x);
printf("&y = %p\n", &y);
int *p = (int *)&j;
4:用于指针类型间的强制转换,用于整数和指针类型间的强制转换
在c语言中,可以直接给指针赋值,但是在c++中不可以,必须强制转换成适当的地址类型;
int *p = (int *)123;
Auto在c++中的作用是让编译器根据赋值的类型决定此变量的值
auto x1 = 0.0;
auto x2 = 0;
这里x1是double类型, x2是整形的,但是本人试了g++,vs2008 都没有编译通过,网上说版本问题,我就先不管了,知道有就行了;
对于const 变量,如果又用volatile修饰过的话呢??
volatile const int y = 2;
int *p = (int *)&y;
*p = 3;
cout<<y<<endl;
这样的话,就会把y声明成了只读变量。
当const的类型变量 如果与初始化的变量类型相同,那么初始化变量称为真正常量,而如果不同,那么就是只读变量并且可以通过指针修改它的值。
注意这里不能
const int j = 1.9;
int *p = const_cast<int*>(&j);
*p = 10;
cout<<j<<endl;
上面的j仍然是常量,而下面的是符合上边红字的说法:
float b = 1.9;
const int j = b;
int *p = const_cast<int*>(&j);
*p = 10;
cout<<j<<endl;
struct SV
{
int x;
int y;
int z;
};
struct SR
{
int &x;
int &y;
int &z;
};
SV s1 = {1,2,3};
SR s2 = {s1.x, s1.y, s1.z};
这样的话, SV的地址是多少呢? SR的地址是多少呢? SV所占的空间是多少呢? SR所占的空间是多少呢??
这里要说一下,要把引用当指针对待,这个上面已经说过了,所以s1的地址和s2的地址是不一样的,并且应该相差12个字节,然后s2.x,s2.y,s2.z2指向的内存地址就是s1中的x,y,z的地址,但是引用自身也是要占有内存的。
说一下数组的替代:
模板vector:是一种动态的实现的,利用了new,和delete,所以申请的数组是在堆中,但是这种效率很低,如果是固定的长度,还是使用正常数组。并且如果要是用容器的话,对于迭代器,比如
vector<int> vec;
vector<int>::iterator p = vec.begin();
p++;//这种效率会低一些,因为要先生成一个临时变量,然后返回临时变量,在进行加加
++p;//这种效率就高了,因为不用生成临时变量
模板array:长度是固定的,效率和正常数组一样,并且所占内存是在栈上的。
在类中定义了成员函数,还有成员变量,那么利用sizeof()这个函数进行检测的时候,例如:下面的例子,输出结果就是8,因为成员变量不是公共的,但是函数这个是公共的,只要在使用的时候调用即可。
class Parents
{
private:
static int c;
void show()
{
static int a = 0;
int i;
i = 0;
cout<<i<<endl;
}
public:
int a;
int b;
};
cout<<sizeof(Parents)<<endl;
这里函数和静态变量都是属于这个类的所以他们不与每个类变量绑在一块,只有在使用的时候去指定的内存取出来即可。
但是如果再向其中加入另一种函数,即虚函数呢??
class Parents
{
private:
static int c;
void show()
{
static int a = 0;
int i;
i = 0;
cout<<i<<endl;
}
public:
int a;
int b;
virtual void func()
{}
};
cout<<sizeof(Parents)<<endl;
因为虚函数,在类中是要有一个虚函数表的,所以类中要有一个指针,指向这个虚函数在虚函数表的位置,指针的大小在32位中是4个字节,在64位下是8字节
对于类,在c++中有两种方法使用类的构造函数初始化变量:
1:Students s1 = Students("Hell", 20, 1.25);
2:Students s2 ("Hell", 20, 1.25);
这两种方法,第二种会效率高,因为第一种会先生成临时变量,然后再调用复制函数,但是第二个不会直接调用构造函数,(这里有个事情就是现在的编译器,一般都会认为这两个是一样的,都是会直接调用构造函数);系统也会为我们定义拷贝构造函数:
Students(const Students &s)
{
...
}
这里如果你定义了拷贝构造函数了的话,系统也就不会在生成构造函数了。
这里需要注意,对于拷贝构造函数的参数,在自己定义的时候一定要使用引用,如果参数不是引用,而是值传递的话,编译器会报错,原因在于当我们把实参传给形参的时候,又会调用一次拷贝构造函数
而在进行拷贝构造函数的时候又会进行调用,这就会造成不停的调用拷贝构造函数,所以一定要使形参为引用,这样才不会出现这种情况。
然后说一下什么时候会调用拷贝构造函数,一共有4种情况:
1当用类的一个对象初始化该类的另一个对象时.例如:
-
C/C++ code
-
int main() { point A( 1 , 2 ); point B(A); // 用对象A初始化对象B,拷贝构造函数被调用. }
2 如果函数的形参是类的对象,调用函数时,进行形参和实参结合时.
-
C/C++ code
-
void f(point p) { } main() { point A( 1 , 2 ); f(A); // 函数的形参为类的对象时,当调用函数时,拷贝构造函数被调用. }
-
C/C++ code
-
point g() { point A( 1 , 2 ); return A; // 函数的返回值是类的对象,返回函数值时,调用拷贝构造函数. } void main() { point B; B = g(); }
4、需要产生一个临时类对象时。
const Students s1("Hell", 20, 1.25);
s1.show();//这里会出现一些问题,因为不确保show会不修改
//s1的值,但是show没有参数,没法为参数添加const,所以
//就要在函数的声明还有定义的地方 变成
void show() const;
void show() const
{
.......
}
这里要注意,如果成员函数不改变成员变量的值的话,就最好在后面加上const以免出问题;
class M
{
private:
int mI;
public:
M(int i)
{
}
int getI()
{
}
};
class Test
{
private:
const int c;//这里的c是一个只读变量
M m1;
M m2;
Test() : /*后面的是初始化列表,不要忘了前边还有冒号哦*/ m1(1), m2(2), c(2) //后边没有分号
//这里c是要在定义的时候就要初始化的,但是不能在类中定义的时候初始化变量
//所以就提供了初始化列表,可以进行初始化,并且只能在这里初始化
{
}
};
注意一下,那个初始化列表的初始化顺序,并不是按照列表里写的顺序进行初始化的,而是按照之前自己定义的顺序进行初始化的的;这个初始化列表要出现在函数定义的后边,不要出现在声明的后面;如果有很多的初始化函数,那么每个初始化函数的后面都可以有初始化列表,并且可以不一样
class M
{
private:
static int i;
int a;
int b;
public:
M()
{
}
void show();
static int get_s();
};
int M::get_s()
{
}
int M::i = 0;
对于类中的静态成员,在声明的时候,系统并不为它分配内存,在使用之前必须对其进行初始化,初始化是在类定义的外边进行初始化,不能在初始化列表进行初始化。静态成员函数是可以访问静态成员的,并且也可以修改静态成员。也是属于类的,所以不能访问a,b这些属于对象的成员;
在写复制构造函数的时候,应该要考虑着这个问题,就是如果在类中有一个指针,这个指针指向一块内存,然后这个类使用完毕后,肯定会调用析构函数,将这块内存释放掉。这时候如果说有一个函数:void Call(Students s); 当我们把变量穿进去的时候,这个函数会产生一个临时变量,然后将s的成员全部复制过去,注意这时候临时变量的指针也指向了同一块内存,当这个函数运行完毕后,临时变量会调用析构函数,将这块内存释放掉,导致意外出现,所以这里一定要注意复制构造函数的写法,赋值运算也是一个样。
对于操作符重载,这里有几个函数必须通过成员函数进行重载:
=,[],(),->;
对于++这个如何分辨是前置还是后置呢?
Students operator++(int)//obj++
Students operator++()//++obj
尽量使用前置++这个原因前边已经说过了;
对于操作符重载,有两个尽量不要重载的操作符,就是 && || 这两个虽然可以重载,但是尽量不要重载。原因是:
( a1 && a1+a2 ) 如果a1不成立,正常是a1+a2是不能使用的,但是重载的话就有可能没注意到这个问题,结果a1+a2也进行运算了,||这个也是一样的
还有就是关于操作符重载后的运算顺序:
T1+T2+T3:正常的顺序是先算t1+t2然后在把和加上t3,但是如果把+这个操作符重载后,就不是这样了,t1.operator(t2+t3) ---> t1.operator(t2.operator(t3))这个是调用的顺序,所以一定要注意顺序;
重载的限制:1:必须有一个参数是用户自己定义的类型
2:如果原来的操作符是双目运算,就不能改成单目运算;
3:不能修改优先级;
对于操作符重载需要做参数是调用类
例如:Students s1;
Students s2;
s2 = s1 + 1.2;
但是:s2 = 1.2 + s1;
就必须重新定义一个了:operator+(double, Students& s)
对于<<操作符必须是友元函数,因为要是累成员函数的话
Students s1; 调用的话就必须s1<<cout;这是违背常识的,所以应该声明称友元函数,调用者是ostream;
转换函数:1:必须是类方法
2:不能指定返回类型;
3:不能有参数
class Students
{
private:
int i;
char c;
public:
operator int() const;//定义了一个转换函数
};
Students::operator int() const//对转换函数进行定义
{
return (int)c;
}
继承:默认为私有继承
对于派生类构造函数:
RatePlayer::RatePlayer(unsigned int r, const string &f, const string &ln, bool t) : TableTennis(r,f,ln)
{
ra = t;
}
RatePlayer是TableTennis的派生类,在RatePlayer中的构造函数调用父类的构造函数;如果在派生类的构造函数中没有调用父类的构造函数,那么编译器就会自动调用默认构造函数。
基类的引用和指针可以指向其派生类。这个好处就是对于一个函数,他的参数是一个指向基类的指针或者引用,那么传递的值就可以是派生类的引用或者指针。
函数重写就是在子类中定义与父类函数一样的函数;但是会覆盖掉父类的函数如果想调用父类的函数:child.Parent::print();
虚函数的作用是为了让指向基类的指针能够通过观察传进来的是基类还是派生类然后在选择调用的是基类的还是派生类的重写函数。
记住子类的函数无法与父类发生重载,因为发生了覆盖。注意虚函数必须返回类型什么的都要一模一样。
对于虚函数,首先要确定调用的是否是虚函数,然后就要调用对象中的虚函数,这里会有一个查找的过程,所以不要把所有的函数都设置成虚函数。
对于继承有时需要对基类析构函数也设置成虚函数:
class Parents
{
private:
int n;
public:
virtual void show()
{
cout<<"Parents"<<endl;
}
~Parents()
{
cout<<"Parents xigou"<<endl;
}
};
class Child : public Parents
{
private:
int a;
public:
void show()
{
cout<<"Child"<<endl;
//return 0;
}
~Child()
{
cout<<"Child xigou"<<endl;
}
};
void run()
{
Parents *p = new Child;
delete p;//这样才可以将Child和Parents的析构函数都调用
//如果不讲基类设置成虚函数,则只会调用基类析构函数
}
构造函数不能使用虚函数;
如果在基类中函数发生重载,那么派生类中最好将所有函数重新定义,如果只定义了一个,那么其他的将被隐藏。
为什么protected介于private与public之间呢?
春虚函数:virtual double Area() const = 0;
这种函数不能被定义,只能作为基类使用。
函数模板可以像普通函数一样进行重载:
int Max(int a, int b) {...}
template <typename T>
T Max(T a, T b) {...}
C++会优先选择普通函数只有类型不匹配时c++会考虑模板。
Cout<<Max(a,b)<<endl;//会优先考虑普通函数
Cout<<Max<>(a,b)<<endl;//由于空的列表让c++只适用函数模板
template <typename T>
T Max(T a, T b) {...}
template <typename T>
T Max(T a, T b, T c) {...}
上边的也属于函数重载
template <typename T>
class Ope
{
public:
T add(T a, T b);
};
//在外部定义的时候也要重新写下一行
template <typename T>
T Ope::add(T a, T b)
{
...
}
Ope<int> a;//定义变量的时候必须指定类型
在基类中声明了一个友元函数,然后再派生类中也声明了一个友元函数,如果想调用基类的友元函数,可以通过将派生类的变量进行强制转换。
设置成员函数为友元函数:在TV类中将Remote类中的一个函数声明为友元函数,那么必须按下面的顺序进行:
class TV;
class Remote
{
...
};
class TW
{
...
};
对于const int a,有两个函数:
void test1(int i)
{
}
void test2(int &i)
{
}
对于const第一个函数编译通过,第二个不可以。
使用指针的引用来实现两个指针的交换
void test(int *&a, int *&b)
{
int *p = a;
a = b;
b = p;
}
int a = 0;
int b = 0;
int *p1 = &a;
int *p2 = &b;
test(p1, p2);