Let me start answering this one differently.
struct Value
{
int nValue;
float fOtherValue;
};
int main()
{
Value theValue;
theValue.nValue = 220;
theValue.fOtherValue = 3.14f;
printf("Value: %d", theValue);
}
This code will print 220 without any issues. But if you pass second argument after theValue, it wont:
printf("Values: %d, %f", theValue, theValue.fOtherValue);
Since the first variable argument theValue doesn't meet the size specified by %d argument. Hence, 3.14 wont (may not) be displayed. Let's not talk how printf, argument pushing on stack, va_start and similar things work.
Similarly, when we design a String class this way:
struct String
{
char* pData;
public:
String()
{
pData = new char[40];
strcpy_s(pData, 40, "Sample");
}
};
And use it this way:
String str;
printf("%s", str);
It would definitely work.
But what about argument and call-stack corruption? What if size of a class (like Value) is more than the data size (4 for Value) as specified by format (%d). Well, in that case, the class designed need to ensure that size of given class is same as argument-size being used by printf function.
I wont mention the details about it, but CString uses internal class CStringData. The latter class holds the string, length, reference count etc. CString (actually CSimpleStringT) uses this class, but hold only the pointer of char/wchar_t, managed by CStringData.
Largely, CString is implemented as the String class mentioned (as far as data and sizeof goes).